IchiMozzi 모바일 앱 개발 - React Native와 Expo로 구현한 학습 UI/UX
들어가며
사용자가 학습을 지속할 수 있는 동기를 제공하려면 직관적이고 매력적인 사용자 인터페이스가 필수입니다. IchiMozzi 모바일 앱은 React Native와 Expo를 활용하여 iOS와 Android 플랫폼에서 일관된 학습 경험을 제공하도록 설계되었습니다.
이번 글에서는 실제 코드와 함께 IchiMozzi의 프론트엔드 아키텍처와 핵심 화면 구현 과정을 공유합니다.
앱 아키텍처 설계
Context API 기반 상태 관리


앱 전체에서 사용자 인증 상태와 푸시 토큰을 효율적으로 관리하기 위해 Context API를 활용했습니다:
// frontend/src/contexts/AuthContext.tsx
import React, { createContext, useState, useContext, useEffect } from 'react';
import { registerForPushNotificationsAsync } from '../utils/registerForPushNotificationsAsync';
type AuthContextType = {
accessToken: string | null;
pushToken: string | null;
setAccessToken: (token: string | null) => void;
setPushToken: (token: string | null) => void;
};
const AuthContext = createContext<AuthContextType>({
accessToken: null,
pushToken: null,
setAccessToken: () => {},
setPushToken: () => {},
});
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const [accessToken, setAccessToken] = useState<string | null>(null);
const [pushToken, setPushToken] = useState<string | null>(null);
// 앱 시작 시 자동으로 푸시 알림 토큰 등록
useEffect(() => {
(async () => {
const token = await registerForPushNotificationsAsync();
setPushToken(token);
})();
}, []);
return (
<AuthContext.Provider value=>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => useContext(AuthContext);
Context API 선택 이유:
- 단순성: Redux 대비 설정이 간단하고 학습 곡선이 낮음
- 타입 안전성: TypeScript와 완벽한 통합
- 성능: 소규모 앱에서 충분한 성능과 메모리 효율성
네비게이션 구조 설계
React Navigation을 활용하여 직관적인 앱 플로우를 구현했습니다:
// frontend/src/navigation/RootNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { AuthStack } from './AuthStack';
import { AppTabs } from './AppTabs';
const RootStack = createStackNavigator();
export function RootNavigator() {
return (
<NavigationContainer>
<RootStack.Navigator screenOptions=>
{/* 인증 플로우 */}
<RootStack.Screen name="AuthStack" component={AuthStack} />
{/* 메인 앱 */}
<RootStack.Screen name="AppTabs" component={AppTabs} />
</RootStack.Navigator>
</NavigationContainer>
);
}
네비게이션 설계 원칙:
- 명확한 플로우: 로그인 → 메인 앱으로 이어지는 직관적인 사용자 여정
- 타입 안전성: 화면 간 파라미터 전달 시 TypeScript 타입 검증
- 백 버튼 최적화: Android 백 버튼 동작 고려
핵심 화면 구현
홈 화면: 개인화된 학습 대시보드
사용자의 학습 상태를 한눈에 보여주는 홈 화면을 구현했습니다:
// frontend/src/screens/HomeScreen.tsx
export function HomeScreen({ navigation }: any) {
const [score, setScore] = useState<number>(0);
const [name, setName] = useState<string>('');
const [level, setLevel] = useState<string>('');
const [problemCount, setProblemCount] = useState<number>(0);
const { accessToken } = useAuth();
// 평균 점수 계산 (총점 3000점 기준)
const avgScore = Math.round(score / 3);
const loadMyInfo = async () => {
try {
if (!accessToken) return;
const myInfo = await fetchMyInfo(accessToken);
setScore(myInfo.score);
setName(myInfo.name);
setLevel(myInfo.currentLevel);
setProblemCount(myInfo.problemCount);
} catch (error: any) {
Alert.alert('오류', error.response?.data?.message || '정보 불러오기 실패');
}
};
// 화면 포커스될 때마다 최신 데이터 로드
useFocusEffect(
useCallback(() => {
loadMyInfo();
}, [accessToken])
);
const handleStudyType = (typeOrTag: string, isTag: boolean = false) => {
navigation.navigate('QuizScreen', {
filterMode: isTag ? 'tag' : 'type',
filterValue: typeOrTag,
caller: 'Home',
});
};
return (
<SafeAreaView style={styles.container}>
<ScrollView contentContainerStyle={styles.scrollContainer}>
{/* 사용자 정보 카드 */}
<View style={styles.greetingCard}>
<Text style={styles.greetingText}>안녕하세요, {name}님!</Text>
<Text style={styles.infoText}>예상 JLPT 등급: {level}</Text>
<Text style={styles.infoText}>현재 실력 점수: {avgScore} / 1000</Text>
<Text style={styles.infoText}>남은 문제: {problemCount} 개</Text>
</View>
{/* 학습 방식 선택 */}
<Text style={styles.sectionTitle}>학습 방식 선택</Text>
<View style={styles.buttonGroup}>
<TouchableOpacity
style={styles.customButton}
onPress={() => handleStudyType(level, true)}
>
<Text style={styles.customButtonText}>사용자 맞춤 학습</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.customButton}
onPress={() => handleStudyType('vocabulary')}
>
<Text style={styles.customButtonText}>추천 단어 학습</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.customButton}
onPress={() => handleStudyType('reading')}
>
<Text style={styles.customButtonText}>추천 읽기 학습</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.customButton}
onPress={() => handleStudyType('grammar')}
>
<Text style={styles.customButtonText}>추천 문법 학습</Text>
</TouchableOpacity>
</View>
</ScrollView>
</SafeAreaView>
);
}
홈 화면 설계 핵심:
- 실시간 데이터:
useFocusEffect
로 화면 진입 시마다 최신 학습 데이터 반영 - 직관적 정보: 점수를 1000점 기준으로 정규화하여 이해하기 쉽게 표시
- 맞춤형 학습: 사용자 레벨에 따른 개인화된 학습 경로 제공
반응형 UI 구현
다양한 디바이스 크기에 대응하는 반응형 디자인을 구현했습니다:
// frontend/src/utils/normalize.ts를 사용한 스타일링
const styles = StyleSheet.create({
greetingCard: {
backgroundColor: '#fff',
borderRadius: normalize(8, 8, 12),
padding: normalize(20, 20, 24),
marginVertical: normalize(10, 10, 15),
elevation: 3,
shadowColor: '#000',
shadowOffset: { width: 0, height: normalize(2, 2, 3) },
shadowOpacity: 0.1,
shadowRadius: normalize(4, 4, 6),
},
greetingText: {
fontSize: normalize(26, 26, 32),
fontWeight: 'bold',
marginBottom: normalize(10, 10, 12),
color: '#333',
},
customButton: {
backgroundColor: '#007AFF',
paddingVertical: normalize(14, 14, 18),
paddingHorizontal: normalize(20, 20, 24),
borderRadius: normalize(8, 8, 10),
marginBottom: normalize(10, 10, 12),
alignItems: 'center',
shadowColor: '#007AFF',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.2,
shadowRadius: normalize(4, 4, 6),
elevation: 2,
},
});
학습 분석 시각화
실력 분석 화면


사용자의 학습 데이터를 시각적으로 분석할 수 있는 차트 기능을 구현했습니다. 어휘, 문법, 독해 영역별 상세 분석을 통해 사용자가 자신의 취약점을 명확히 파악할 수 있도록 했습니다.
학습 로그 관리
일별, 주별, 월별로 학습 활동을 추적하고 기록하는 기능을 구현했습니다. 사용자가 자신의 학습 패턴을 이해하고 지속적인 학습 동기를 유지할 수 있도록 돕습니다.
오답 노트 시스템
유연한 그룹 관리


사용자가 틀린 문제들을 주제별, 난이도별로 자유롭게 그룹화할 수 있는 오답 노트 시스템을 구현했습니다. 이를 통해 개인화된 복습 전략을 세울 수 있도록 지원합니다.
학습 모드 구현
집중 학습 모드


사용자가 학습에 집중할 수 있도록 미니멀한 인터페이스로 설계된 문제 풀이 화면입니다. 실시간 피드백과 함께 사용자의 학습 데이터를 백엔드로 전송하여 개인화된 분석이 가능하도록 구현했습니다.
API 통신 최적화
타입 안전한 API 호출
백엔드와의 안전한 통신을 위해 TypeScript 기반의 API 클라이언트를 구현했습니다:
// frontend/src/api/auth.ts
const API_URL = 'https://ichimozzi.com';
export async function registerUser(
name: string,
email: string,
password: string,
level: string
) {
const response = await axios.post(`${API_URL}/auth/register`, {
name,
email,
password,
level,
});
return response.data;
}
export async function loginUser(email: string, password: string) {
const response = await axios.post(`${API_URL}/auth/login`, {
email,
password,
});
return response.data;
}
// 게스트 로그인으로 진입 장벽 낮추기
export async function guestLogin() {
const response = await axios.post(`${API_URL}/auth/guest`);
return response.data;
}
API 설계 특징:
- 명확한 타입: 모든 API 요청/응답에 TypeScript 타입 적용
- 에러 핸들링: 사용자 친화적인 에러 메시지 제공
- 게스트 지원: 가입 없이도 앱 체험 가능
사용자 경험 최적화
성능 최적화 기법
- useFocusEffect 활용: 필요한 시점에만 데이터 로드
- 메모이제이션: 불필요한 리렌더링 방지
- 이미지 최적화: 적절한 해상도와 압축률 적용
접근성 고려사항
- 터치 영역: 44pt 이상의 충분한 터치 영역 확보
- 색상 대비: WCAG 가이드라인 준수
- 피드백: 모든 사용자 액션에 대한 즉각적 피드백 제공
Expo 활용 이점
개발 생산성 향상
// package.json - Expo 기반 설정
{
"dependencies": {
"expo": "~52.0.27",
"expo-notifications": "^0.29.13",
"expo-device": "~7.0.2",
"react-native": "0.76.6"
}
}
Expo 선택의 핵심 이점:
- 빠른 개발: 네이티브 모듈 설정 없이 즉시 개발 시작
- OTA 업데이트: 앱스토어 승인 없이 실시간 업데이트 배포
- 풍부한 API: 푸시 알림, 디바이스 정보 등 쉬운 접근
배포 최적화
Expo Application Services(EAS)를 활용하여 효율적인 빌드와 배포 프로세스를 구축했습니다:
// eas.json - 빌드 설정
{
"cli": {
"version": ">= 13.2.1"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"autoIncrement": true
}
}
}
배운 점과 개선 사항
성공 요인
- 사용자 중심 설계: 학습자의 실제 니즈를 반영한 UI/UX
- 타입 안전성: TypeScript로 런타임 오류 최소화
- 모듈화: 재사용 가능한 컴포넌트 구조
개선할 점
- 오프라인 지원: 네트워크 연결 없이도 기본 기능 사용 가능하도록 개선
- 애니메이션: 더 부드러운 사용자 경험을 위한 마이크로 인터랙션 추가
- 테스트: 컴포넌트 테스트 커버리지 확대
다음 단계
다음 글에서는 사용자 인증 시스템의 보안 구현과 JWT 토큰 관리, 그리고 게스트 로그인 시스템의 설계 과정을 다룰 예정입니다.
특히 사용자 진입 장벽을 낮추면서도 보안을 유지하는 방법과, 다양한 인증 방식을 통합 관리하는 아키텍처에 대해 상세히 공유하겠습니다.
React Native와 Expo를 활용한 모바일 앱 개발을 통해 사용자 경험과 개발 생산성을 동시에 고려한 크로스 플랫폼 개발의 장점을 깊이 체험할 수 있었습니다.
Enjoy Reading This Article?
Here are some more articles you might like to read next: