React Native 학습 노트: 섹션 1-7 통합 정리

이 문서는 섹션 1부터 섹션 7까지의 강의 노트를 노션 한 페이지에 붙여넣기 좋게 재구성한 통합 정리다.

핵심 흐름은 React 지식 재사용 -> React Native 환경 적응 -> 앱 UI 구현 -> 반응형/플랫폼 대응 -> 내비게이션 -> 앱 전역 상태 관리다.


전체 로드맵

섹션주제핵심 질문
1React Native와 Expo 시작React Native는 웹 React와 무엇이 같고 무엇이 다른가?
2핵심 컴포넌트, 스타일링, 상태, 목록HTML/CSS 대신 어떤 컴포넌트와 스타일 시스템을 쓰는가?
3디버깅브라우저가 아닌 모바일 앱은 어떻게 디버깅하는가?
4숫자 맞히기 게임 앱RN 기본기를 실제 앱 구조로 어떻게 조합하는가?
5반응형, 방향, 플랫폼, 상태바화면 크기, 회전, 키보드, 플랫폼 차이에 어떻게 대응하는가?
6React Navigation여러 화면 사이를 어떻게 자연스럽게 이동시키는가?
7Context와 Redux여러 화면이 공유하는 상태를 어떻게 관리하는가?

가장 큰 관점 전환

React Native는 React를 새로 배우는 것이 아니라, React를 브라우저 DOM이 아닌 모바일 네이티브 UI에 연결하는 방식이다.

웹 React
React + react-dom
-> 브라우저 DOM
-> HTML / CSS / browser event

React Native
React + react-native
-> iOS / Android 네이티브 UI
-> View / Text / Pressable / native event

프론트엔드 개발자가 계속 가져가는 것은 React의 사고방식이다.

  • 컴포넌트
  • JSX
  • props
  • state
  • hooks
  • 조건부 렌더링
  • 이벤트 핸들러
  • 리스트 렌더링
  • 컴포넌트 분리
  • Context / Redux 같은 전역 상태 관리

새로 익혀야 하는 것은 렌더링 대상과 모바일 환경이다.

  • DOM이 없다.
  • HTML 태그를 쓸 수 없다.
  • CSS 파일과 cascade가 없다.
  • 브라우저 DevTools만으로 끝나지 않는다.
  • 실제 기기, 에뮬레이터, 시뮬레이터에서 확인한다.
  • 키보드, safe area, status bar, 화면 회전, 플랫폼 차이를 직접 고려한다.

섹션 1. React Native와 Expo 시작

한 줄 요약

React Native는 React의 컴포넌트와 상태 관리 방식을 그대로 사용하되, 결과물을 브라우저 DOM이 아니라 iOS와 Android의 네이티브 UI로 렌더링한다.

핵심 내용

  • React Native는 React 기반 모바일 앱 프레임워크다.
  • JSX로 작성한 RN 컴포넌트는 각 플랫폼의 네이티브 UI 요소로 연결된다.
  • JavaScript 로직은 앱 내부 JS 런타임에서 실행된다.
  • Expo는 RN 프로젝트 생성, 실행, 기기 미리보기, 네이티브 기능 접근을 쉽게 해주는 도구다.
  • 웹에서는 localhost 브라우저가 중심이지만, RN에서는 Expo Go, Android Emulator, iOS Simulator가 중심이다.

Expo 프로젝트 흐름

npx create-expo-app --template blank
npm start

자주 보는 파일은 다음과 같다.

항목역할
App.js앱 루트 컴포넌트
app.jsonExpo 앱 설정
assets이미지, 아이콘, 폰트 등 정적 자산
package.json의존성, 실행 스크립트

프론트엔드 관점 포인트

  • react-dom 대신 react-native가 렌더링 환경을 담당한다.
  • 웹의 HTML/CSS 지식은 직접 사용되지 않지만, 레이아웃과 컴포넌트 사고는 이어진다.
  • iOS Simulator는 macOS와 Xcode가 필요하다.
  • 실제 기기에서 Expo Go로 QR 코드를 스캔해 빠르게 확인할 수 있다.

섹션 1 체크리스트

  • React Native가 React를 네이티브 UI에 연결하는 프레임워크임을 설명할 수 있다.
  • Expo의 역할을 Vite/Next CLI 같은 개발 도구 관점에서 이해한다.
  • App.js, app.json, assets, package.json의 역할을 구분한다.
  • 브라우저 대신 기기/시뮬레이터에서 앱을 확인하는 흐름에 익숙하다.

섹션 2. 핵심 컴포넌트, 스타일링, 상태, 목록

한 줄 요약

React Native의 핵심 컴포넌트, 스타일링, 이벤트, 상태, 목록 렌더링을 사용해 첫 모바일 앱을 만들며 웹 React와 네이티브 앱 개발의 차이를 익히는 섹션이다.

웹 요소와 RN 컴포넌트 대응

웹 개념React Native
divView
텍스트 태그Text
inputTextInput
buttonButton, Pressable
imgImage
스크롤 컨테이너ScrollView
긴 목록FlatList
팝업/오버레이Modal

가장 중요한 규칙은 텍스트는 반드시 Text 컴포넌트 안에 있어야 한다는 점이다.

// 잘못된 방식
<View>Hello</View>

// 올바른 방식
<View>
  <Text>Hello</Text>
</View>

스타일링

React Native에는 CSS 파일 대신 JavaScript 스타일 객체를 사용한다.

const styles = StyleSheet.create({
  container: {
    flex: 1,
    padding: 16,
    backgroundColor: '#1e085a',
  },
});

주요 차이:

  • background-color가 아니라 backgroundColor처럼 camelCase를 쓴다.
  • 숫자 값은 디바이스 밀도를 고려한 논리 픽셀처럼 다뤄진다.
  • CSS cascade가 없다.
  • 부모 View의 글자색이 자식 Text에 자동 상속되지 않는다.
  • 모든 컴포넌트가 모든 스타일 속성을 지원하지는 않는다.
  • iOS와 Android에서 같은 스타일이 다르게 보일 수 있다.

Flexbox

RN 레이아웃의 중심은 Flexbox다. 단, 기본 방향이 웹과 다르다.

웹 CSS Flexbox 기본 방향: row
React Native Flexbox 기본 방향: column

자주 쓰는 패턴:

container: {
  flex: 1,
  flexDirection: 'row',
  justifyContent: 'center',
  alignItems: 'center',
}

이벤트와 상태

React의 상태 관리 패턴은 그대로 쓴다.

const [enteredGoalText, setEnteredGoalText] = useState('');

웹과 다른 이벤트 이름:

<TextInput
  onChangeText={setEnteredGoalText}
  value={enteredGoalText}
/>

<Button title="Add Goal" onPress={addGoalHandler} />
웹: onChange / onClick
RN: onChangeText / onPress

ScrollView와 FlatList

ScrollView는 내부 항목을 모두 렌더링한다.

  • 짧고 제한된 콘텐츠에 적합하다.
  • 긴 동적 목록에는 비효율적일 수 있다.

FlatList는 긴 목록에 적합하다.

<FlatList
  data={courseGoals}
  renderItem={(itemData) => (
    <GoalItem text={itemData.item.text} />
  )}
  keyExtractor={(item) => item.id}
/>

컴포넌트 분리 흐름

App
-> 전체 상태와 목표 목록 관리

GoalInput
-> 목표 입력 UI와 입력 상태 관리

GoalItem
-> 개별 목표 항목 렌더링과 삭제 이벤트 연결

섹션 2 체크리스트

  • View, Text, TextInput, Button, Pressable, Image, Modal의 역할을 구분한다.
  • RN 스타일 객체와 CSS의 차이를 설명할 수 있다.
  • Flexbox 기본 방향이 column임을 기억한다.
  • onChangeText, onPress를 자연스럽게 사용할 수 있다.
  • ScrollViewFlatList의 차이를 알고 목록 성능을 고려한다.
  • 상태와 콜백을 props로 내려 컴포넌트를 분리할 수 있다.

섹션 3. 디버깅

한 줄 요약

React Native 디버깅은 웹에서 익숙한 에러 메시지 읽기, console.log, DevTools 활용 습관을 모바일 앱 실행 환경에 맞게 옮겨 쓰는 과정이다.

디버깅 기본 순서

1. 에러 메시지를 읽는다.
2. 터미널 로그를 확인한다.
3. 스택트레이스에서 직접 만든 컴포넌트를 찾는다.
4. console.log로 값과 흐름을 확인한다.
5. Expo 개발자 메뉴를 연다.
6. 필요하면 Chrome DevTools나 React DevTools를 사용한다.
7. 공식 문서로 컴포넌트 prop 사용법을 확인한다.

에러 메시지 읽기

RN 에러는 앱 화면과 터미널에 표시된다. 중요한 단서는 보통 다음이다.

  • 어떤 컴포넌트에서 문제가 났는가
  • 어떤 prop이나 값이 문제인가
  • 어떤 파일/컴포넌트 흐름에서 발생했는가

스택트레이스에서는 RN 내부 코드보다 직접 만든 컴포넌트 이름을 찾는 것이 중요하다.

console.log

console.log('현재 입력값:', enteredGoalText);

출력은 npm start를 실행한 터미널에서 확인한다.

확인할 수 있는 것:

  • 컴포넌트 렌더링 시점
  • 이벤트 핸들러 호출 여부
  • state 값 변화
  • props 전달 여부
  • 여러 기기에서 실행될 때 로그가 여러 번 찍히는 이유

Expo 단축키

단축키역할
aAndroid Emulator에서 앱 열기
iiOS Simulator에서 앱 열기
r앱 새로고침
m개발자 메뉴 열기/닫기
?단축키 목록 보기

개발자 메뉴

여는 방법:

터미널: m
iOS Simulator: Command + D
Android Emulator: Ctrl + M

사용 목적:

  • 앱 새로고침
  • 원격 JS 디버깅
  • 개발 도구 실행
  • 앱 홈 이동

React DevTools

React Native에서는 독립 실행형 React DevTools를 사용할 수 있다.

npm install -g react-devtools
react-devtools

확인 가능한 것:

  • 컴포넌트 트리
  • props
  • state
  • state 수정에 따른 UI 변화

섹션 3 체크리스트

  • RN 에러 메시지에서 컴포넌트와 prop 단서를 찾을 수 있다.
  • 터미널 로그와 앱 화면 에러를 함께 확인한다.
  • Expo 개발자 메뉴를 열 수 있다.
  • console.log로 이벤트와 상태 흐름을 추적할 수 있다.
  • 독립 실행형 React DevTools의 용도를 안다.

섹션 4. 숫자 맞히기 게임 앱

한 줄 요약

React Native 기본 컴포넌트와 React 상태 관리 지식을 바탕으로 숫자 맞히기 게임 앱을 만들며 모바일 앱다운 화면 구성, 커스텀 UI, 에셋, 폰트, 아이콘, 목록, 상태 기반 화면 전환을 종합적으로 연습한다.

앱 화면 흐름

StartGameScreen
-> 사용자가 숫자 입력

GameScreen
-> 앱이 숫자를 추측하고 사용자가 higher/lower 피드백 제공

GameOverScreen
-> 라운드 수와 선택 숫자 표시
-> 새 게임 시작

상태 기반 화면 전환

이 섹션에서는 React Navigation 없이 App의 상태로 화면을 바꾼다.

let screen =<StartGameScreen onPickNumber={pickedNumberHandler} />;

if (userNumber) {
  screen = (
    <GameScreen
      userNumber={userNumber}
      onGameOver={gameOverHandler}
    />
  );
}

if (gameIsOver && userNumber) {
  screen = (
    <GameOverScreen
      roundsNumber={guessRounds}
      userNumber={userNumber}
      onStartNewGame={startNewGameHandler}
    />
  );
}

간단한 앱에서는 충분하지만, 뒤로가기, 스택, 탭, 헤더, 딥링크가 필요해지면 React Navigation 같은 라이브러리가 필요하다.

커스텀 버튼

기본 Button 대신 Pressable, View, Text로 직접 버튼을 만든다.

<Pressable
  onPress={onPress}
  android_ripple={{ color: Colors.primary600 }}
  style={({ pressed }) =>
    pressed
      ? [styles.buttonInnerContainer, styles.pressed]
      : styles.buttonInnerContainer
  }
>
  <Text style={styles.buttonText}>{children}</Text>
</Pressable>

플랫폼별 피드백:

  • Android: android_ripple
  • iOS: Pressablepressed 상태 기반 opacity

재사용 UI 컴포넌트

components/ui
-> PrimaryButton
-> Title
-> Card
-> InstructionText

components/game
-> NumberContainer
-> GuessLogItem

style prop을 외부에서 받아 기본 스타일과 병합하는 패턴이 중요하다.

function InstructionText({ children, style }) {
  return (
    <Text style={[styles.instructionText, style]}>
      {children}
    </Text>
  );
}

입력 검증과 Alert

사용자 입력은 문자열로 들어오므로 숫자로 변환하고 검증한다.

입력값 읽기
-> 숫자로 변환
-> 1~99 범위인지 확인
-> 유효하지 않으면 Alert 표시
-> 유효하면 게임 화면으로 이동

게임 로직

앱은 사용자가 선택한 숫자를 직접 맞히는 것이 아니라, 범위를 좁히며 추측한다.

minBoundary / maxBoundary
-> higher/lower 피드백에 따라 범위 업데이트
-> 새 난수 생성
-> 정답이면 game over 처리

useEffect는 추측값이 사용자 숫자와 같아졌을 때 게임 종료 콜백을 실행하는 데 사용한다.

에셋과 시각 요소

사용한 요소:

  • expo-linear-gradient로 배경 그라데이션
  • ImageBackground로 배경 이미지 오버레이
  • @expo/vector-icons로 아이콘 버튼
  • expo-font로 커스텀 폰트
  • SafeAreaView로 안전 영역 처리

추측 로그

라운드별 추측 기록을 배열 상태로 관리하고 FlatList로 렌더링한다.

currentGuess
-> allGuesses 배열에 추가
-> FlatList로 GuessLogItem 렌더링
-> 라운드 번호와 추측 숫자 표시

섹션 4 체크리스트

  • 상태 기반 조건부 렌더링으로 간단한 화면 전환을 구현할 수 있다.
  • Pressable로 커스텀 버튼과 플랫폼별 눌림 피드백을 만들 수 있다.
  • 공통 UI 컴포넌트와 도메인 컴포넌트를 구분할 수 있다.
  • 입력값 검증과 Alert 사용 흐름을 이해한다.
  • useEffect로 게임 종료 같은 side effect를 처리할 수 있다.
  • 이미지, 배경, 아이콘, 폰트, safe area를 앱 UI에 적용할 수 있다.
  • FlatList로 게임 로그 목록을 렌더링할 수 있다.

섹션 5. 반응형, 화면 방향, 플랫폼, 상태바

한 줄 요약

React Native에서 웹의 미디어 쿼리와 CSS 반응형 사고를 그대로 사용할 수는 없지만, Dimensions, useWindowDimensions, KeyboardAvoidingView, ScrollView, Platform, 플랫폼별 파일, StatusBar를 조합해 다양한 화면 크기, 방향, 플랫폼에 대응하는 적응형 UI를 만들 수 있다.

크기 제약과 퍼센트 단위

웹의 max-width와 비슷한 사고를 RN에서도 쓸 수 있다.

title: {
  width: 300,
  maxWidth: '80%',
}
큰 화면
-> 300 사용

작은 화면
-> 부모 너비의 80%까지만 사용

퍼센트 값은 부모 컨테이너 기준이다. 크기를 줄인 뒤 가운데 정렬이 깨지면 부모의 alignItems도 함께 조정해야 한다.

Dimensions API

Dimensions.get('window')로 현재 화면 크기를 읽는다.

import { Dimensions } from 'react-native';

const deviceWidth = Dimensions.get('window').width;

조건부 스타일에 사용할 수 있다.

padding: deviceWidth < 380 ? 12 : 24

주의할 점:

  • 파일 최상단에서 한 번 읽으면 초기값처럼 동작한다.
  • 앱 실행 중 기기 방향이 바뀌어도 자동으로 다시 계산되지 않는다.
  • 동적 회전 대응에는 useWindowDimensions가 더 적합하다.

이미지 크기와 원형 마스크

이미지가 width: '100%', height: '100%'를 갖고 있어도 실제 크기는 부모 컨테이너가 결정한다.

원형 이미지는 세 값이 함께 움직여야 한다.

const imageSize = deviceWidth < 380 ? 150 : 300;

imageContainer: {
  width: imageSize,
  height: imageSize,
  borderRadius: imageSize / 2,
}

퍼센트 단위는 너비와 높이 기준이 달라질 수 있으므로 원형 이미지에는 조심해야 한다.

화면 방향 설정

Expo의 app.json에서 화면 방향을 설정한다.

{
  "expo": {
    "orientation": "default"
  }
}

설정 의미:

의미
portrait세로 고정
landscape가로 고정
default기기 방향에 따라 전환

가로 모드를 허용하면 UI가 자동으로 좋아지는 것이 아니라, 높이 부족과 키보드 겹침 같은 문제가 드러난다.

useWindowDimensions

화면 크기나 방향 변경에 동적으로 대응하려면 훅을 사용한다.

const { width, height } = useWindowDimensions();

동적 스타일 예시:

const marginTopDistance = height < 380 ? 30 : 100;

<View style={[styles.rootContainer, { marginTop: marginTopDistance }]} />

구분:

Dimensions.get()
-> 초기 화면 크기 기준 분기

useWindowDimensions()
-> 회전과 크기 변경에 즉시 반응

키보드와 스크롤 대응

모바일에서는 키보드가 입력창과 버튼을 가릴 수 있다.

<ScrollView style={styles.screen}>
  <KeyboardAvoidingView behavior="position" style={styles.screen}>
    ...
  </KeyboardAvoidingView>
</ScrollView>

역할:

  • KeyboardAvoidingView: 키보드가 열릴 때 UI를 밀어 올림
  • ScrollView: 화면이 부족할 때 사용자가 스크롤해서 버튼까지 접근 가능

가로 모드 레이아웃 구조 변경

단순히 margin이나 fontSize만 줄이는 것이 아니라 JSX 구조를 바꿀 수도 있다.

세로
-> NumberContainer
-> Card 안에 higher/lower 버튼
-> 로그 목록

가로
-> 버튼 / NumberContainer / 버튼을 한 줄 배치
-> 불필요한 안내 문구 제거
-> 로그 목록 공간 확보

GameOverScreen 개선

게임 오버 화면에서는 이미지가 버튼을 밀어내지 않도록 화면 너비와 높이를 모두 고려한다.

let imageSize = 300;

if (width < 380) {
  imageSize = 150;
}

if (height < 400) {
  imageSize = 80;
}

const imageStyle = {
  width: imageSize,
  height: imageSize,
  borderRadius: imageSize / 2,
};

전체 화면은 ScrollView로 감싸 버튼 접근성을 보장한다.

Platform API

플랫폼별 스타일 분기는 Platform.OS 또는 Platform.select()로 처리한다.

borderWidth: Platform.OS === 'android' ? 2 : 0
borderWidth: Platform.select({
  ios: 0,
  android: 2,
})

플랫폼별 파일

파일 이름에 플랫폼 확장자를 붙일 수 있다.

Title.android.js
Title.ios.js
colors.android.js
colors.ios.js

import할 때는 플랫폼 확장자를 쓰지 않는다.

import Title from '../components/Title';

React Native가 실행 플랫폼에 맞는 파일을 자동 선택한다.

StatusBar

Expo의 StatusBar로 상태바 스타일을 제어한다.

import { StatusBar } from 'expo-status-bar';

<>
  <StatusBar style="light" />
  <AppContent />
</>

화면 전체 배경이 어두우면 light, 밝으면 dark를 선택한다.

섹션 5 체크리스트

  • width, maxWidth, 퍼센트 단위를 조합해 작은 화면 대응을 할 수 있다.
  • Dimensions.get()useWindowDimensions()의 차이를 안다.
  • 원형 이미지는 width, height, borderRadius를 함께 조정해야 함을 이해한다.
  • KeyboardAvoidingViewScrollView를 조합해 키보드 겹침 문제를 줄일 수 있다.
  • 화면 방향에 따라 스타일뿐 아니라 JSX 구조도 바꿀 수 있다.
  • Platform.OS, Platform.select, 플랫폼별 파일 패턴을 이해한다.
  • Expo StatusBar로 상태바 색상을 제어할 수 있다.

섹션 6. React Navigation

한 줄 요약

React Navigation을 사용해 React Native 앱의 화면 전환을 웹의 URL 라우팅이 아닌 모바일 앱의 네이티브 내비게이션 패턴으로 이해하고, Stack, Drawer, Tabs, params, screen options, header actions, nested navigators를 조합해 여러 화면을 가진 앱 구조를 설계한다.

웹 라우팅과 모바일 내비게이션의 차이


-> URL 입력
-> 링크 클릭
-> /categories/:id 같은 path 중심

모바일 앱
-> 버튼/터치
-> 화면 stack 이동
-> drawer/tabs 전환
-> 뒤로가기 제스처와 헤더

Meals 앱 화면 흐름

초기 흐름:

CategoriesScreen
-> MealsOverviewScreen
-> MealDetailScreen

즐겨찾기 추가 후 최종 흐름:

Stack
-> DrawerNavigator
   -> Categories
   -> Favorites
-> MealsOverview
-> MealDetail

React Navigation 설치

기본 패키지:

npm install @react-navigation/native
npx expo install react-native-screens react-native-safe-area-context

Native Stack:

npx expo install @react-navigation/native-stack

Drawer:

npm install @react-navigation/drawer

Bottom Tabs:

npm install @react-navigation/bottom-tabs

앱 전체 내비게이션 설정을 감싼다.

<NavigationContainer>
  <Stack.Navigator>
    ...
  </Stack.Navigator>
</NavigationContainer>

Native Stack Navigator

const Stack = createNativeStackNavigator();

<Stack.Navigator>
  <Stack.Screen
    name="MealsCategories"
    component={CategoriesScreen}
  />
  <Stack.Screen
    name="MealsOverview"
    component={MealsOverviewScreen}
  />
</Stack.Navigator>

name은 화면 식별자다. 웹의 route path 대신 navigation action에서 사용한다.

화면 이동

Screen으로 등록된 컴포넌트는 navigation prop을 받는다.

function CategoriesScreen({ navigation }) {
  function pressHandler() {
    navigation.navigate('MealsOverview');
  }
}

중첩 컴포넌트에서 직접 이동해야 하면 useNavigation()을 사용할 수 있다.

const navigation = useNavigation();

params 전달과 route

이동하면서 데이터를 넘긴다.

navigation.navigate('MealsOverview', {
  categoryId: itemData.item.id,
});

대상 화면에서 읽는다.

function MealsOverviewScreen({ route }) {
  const catId = route.params.categoryId;
}

params는 화면을 그리는 데 필요한 작은 식별자에 적합하다.

적합
-> categoryId
-> mealId

부적합
-> 즐겨찾기 전체 목록
-> 로그인 사용자
-> 장바구니

데이터 필터링

카테고리 id를 기준으로 meals를 필터링한다.

const displayedMeals = MEALS.filter((mealItem) => {
  return mealItem.categoryIds.indexOf(catId) >= 0;
});

CategoryGridTile

카테고리는 FlatListnumColumns로 2열 그리드를 만든다.

<FlatList
  data={CATEGORIES}
  keyExtractor={(item) => item.id}
  renderItem={renderCategoryItem}
  numColumns={2}
/>

Pressable을 사용해 터치 가능한 grid tile을 구성하고, Android ripple과 iOS opacity 피드백을 준다.

MealItem

Meal 목록은 별도 MealItem 컴포넌트로 렌더링한다.

구성:

  • 원격 이미지
  • 제목
  • 조리 시간
  • 난이도
  • 가격 수준
  • 눌림 피드백
  • 상세 화면으로 이동

원격 이미지는 반드시 크기를 지정해야 한다.

<Image
  source={{ uri: imageUrl }}
  style={styles.image}
/>

screenOptions와 options

공통 화면 설정은 Navigator의 screenOptions에 둔다.

<Stack.Navigator
  screenOptions={{
    headerStyle: { backgroundColor: '#351401' },
    headerTintColor: 'white',
    contentStyle: { backgroundColor: '#3f2f25' },
  }}
>

개별 화면 설정은 Stack.Screenoptions에 둔다.

<Stack.Screen
  name="MealDetail"
  component={MealDetailScreen}
  options={{ title: 'About the Meal' }}
/>

화면별 options가 공통 screenOptions보다 우선한다.

동적 options

선택된 카테고리 이름처럼 화면 데이터에 따라 헤더 제목이 달라질 때 사용한다.

useLayoutEffect(() => {
  navigation.setOptions({
    title: categoryTitle,
  });
}, [navigation, categoryTitle]);

useEffect보다 useLayoutEffect를 쓰면 화면 전환 애니메이션 중 제목이 늦게 바뀌는 느낌을 줄일 수 있다.

MealDetailScreen

route.params.mealId로 선택된 meal을 찾고 상세 정보를 렌더링한다.

const mealId = route.params.mealId;
const selectedMeal = MEALS.find((meal) => meal.id === mealId);

상세 화면 구성:

  • 이미지
  • 제목
  • MealDetails
  • Ingredients
  • Steps

재사용 컴포넌트:

MealDetails
-> duration / complexity / affordability 표시

Subtitle
-> Ingredients / Steps 제목

List
-> ingredients / steps 배열 출력

상세 내용이 길어질 수 있으므로 전체 화면은 ScrollView로 감싼다.

headerRight와 IconButton

헤더 오른쪽에 즐겨찾기 버튼을 추가한다.

navigation.setOptions({
  headerRight: () => (
    <IconButton
      icon="star"
      color="white"
      onPress={changeFavoriteStatusHandler}
    />
  ),
});

IconButtonPressableIonicons로 만든다.

<Pressable onPress={onPress}>
  <Ionicons name={icon} size={24} color={color} />
</Pressable>

Drawer Navigator

사이드 드로어 패턴을 제공한다.

const Drawer = createDrawerNavigator();

<Drawer.Navigator>
  <Drawer.Screen
    name="Welcome"
    component={WelcomeScreen}
  />
  <Drawer.Screen
    name="User"
    component={UserScreen}
  />
</Drawer.Navigator>

Drawer는 기본 헤더와 메뉴 버튼을 제공한다.

화면에서 드로어를 직접 열 수도 있다.

navigation.toggleDrawer();

Bottom Tabs Navigator

하단 탭으로 주요 섹션을 전환한다.

const BottomTabs = createBottomTabNavigator();

<BottomTabs.Navigator
  screenOptions={{
    tabBarActiveTintColor: '#3c0a6b',
  }}
>

탭 아이콘은 tabBarIcon으로 설정한다.

options={{
  tabBarIcon: ({ color, size }) => (
    <Ionicons name="home" color={color} size={size} />
  ),
}}

Nested Navigators

여러 Navigator를 조합할 수 있다.

Stack
-> DrawerNavigator
   -> Categories
   -> Favorites
-> MealsOverview
-> MealDetail

주의할 점:

  • 각 Navigator는 자체 header를 만들 수 있다.
  • 중첩하면 header가 두 개 생길 수 있다.
  • 필요 없는 header는 headerShown: false로 숨긴다.
<Stack.Screen
  name="Drawer"
  component={DrawerNavigator}
  options={{ headerShown: false }}
/>

Drawer의 콘텐츠 배경은 Stack의 contentStyle이 아니라 sceneContainerStyle을 사용한다.

섹션 6 체크리스트

  • NavigationContainer와 Navigator/Screen 구조를 이해한다.
  • navigation.navigate()로 화면 이동을 구현할 수 있다.
  • route.params로 전달된 식별자를 읽을 수 있다.
  • params와 앱 전역 상태의 차이를 구분한다.
  • screenOptions, options, navigation.setOptions()의 차이를 안다.
  • 헤더에 버튼을 추가하고 화면 내부 로직과 연결할 수 있다.
  • Stack, Drawer, Bottom Tabs의 사용 목적을 구분한다.
  • nested navigators에서 header 중복을 해결할 수 있다.

섹션 7. Context와 Redux로 앱 전역 상태 관리

한 줄 요약

여러 화면이 공유해야 하는 즐겨찾기 상태를 Context API와 Redux Toolkit 두 방식으로 구현하며, React Native에서도 앱 전역 상태 관리는 React 웹 앱에서 쓰던 Context/Redux 지식이 거의 그대로 적용됨을 확인한다.

왜 전역 상태가 필요한가

Meals 앱에는 두 화면이 같은 상태를 공유해야 한다.

MealDetailScreen
-> 현재 meal을 favorite으로 추가/삭제
-> favorite 여부에 따라 별 아이콘 변경

FavoritesScreen
-> favorite meal 목록 표시
-> favorite이 없으면 empty state 표시

이 상태는 특정 화면에만 두면 안 된다.

로컬 상태
-> 한 화면에서만 필요

앱 전역 상태
-> 여러 화면에서 읽고 변경

이번 섹션에서 관리하는 전역 상태는 favorite meal id 배열이다.

['m1', 'm3']

params와 전역 상태의 차이

route.params
-> 현재 화면을 여는 데 필요한 작은 식별자
-> categoryId
-> mealId

app-wide state
-> 여러 화면에서 공유하고 계속 변하는 상태
-> favoriteMealIds
-> 로그인 사용자
-> 장바구니
-> 테마

Context API 방식

폴더:

store/context/favorites-context.js

Context 객체는 상태의 공개 API를 정의한다.

export const FavoritesContext = createContext({
  ids: [],
  addFavorite: (id) => {},
  removeFavorite: (id) => {},
});

Provider는 실제 상태와 함수를 관리한다.

function FavoritesContextProvider({ children }) {
  const [favoriteMealIds, setFavoriteMealIds] = useState([]);

  function addFavorite(id) {
    setFavoriteMealIds((currentFavIds) => [
      ...currentFavIds,
      id,
    ]);
  }

  function removeFavorite(id) {
    setFavoriteMealIds((currentFavIds) =>
      currentFavIds.filter((mealId) => mealId !== id)
    );
  }

  const value = {
    ids: favoriteMealIds,
    addFavorite,
    removeFavorite,
  };

  return (
    <FavoritesContext.Provider value={value}>
      {children}
    </FavoritesContext.Provider>
  );
}

Provider는 앱에서 필요한 범위를 감싼다.

<FavoritesContextProvider>
  <NavigationContainer>
    ...
  </NavigationContainer>
</FavoritesContextProvider>

Context 소비

MealDetailScreen에서 favorite 여부 계산:

const favoriteMealsContext = useContext(FavoritesContext);

const mealIsFavorite =
  favoriteMealsContext.ids.includes(mealId);

토글:

if (mealIsFavorite) {
  favoriteMealsContext.removeFavorite(mealId);
} else {
  favoriteMealsContext.addFavorite(mealId);
}

FavoritesScreen에서 원본 데이터 필터링:

const favoriteMeals = MEALS.filter((meal) =>
  favoriteMealsContext.ids.includes(meal.id)
);

MealsList 추출

MealsOverviewScreenFavoritesScreen이 같은 목록 UI를 쓰므로 MealsList로 추출한다.

components/MealsList/MealsList.js
components/MealsList/MealItem.js

화면의 책임:

MealsOverviewScreen
-> categoryId로 displayedMeals 선택
-> MealsList에 items 전달

FavoritesScreen
-> favorite ids로 favoriteMeals 선택
-> MealsList에 items 전달

MealsList
-> FlatList 렌더링

MealItem
-> 개별 meal UI와 detail 이동

Empty state

즐겨찾기가 없을 때는 빈 리스트 대신 안내 문구를 보여준다.

if (favoriteMeals.length === 0) {
  return (
    <View style={styles.rootContainer}>
      <Text style={styles.text}>
        You have no favorite meals yet.
      </Text>
    </View>
  );
}

Redux Toolkit 방식

설치:

npm install @reduxjs/toolkit react-redux

폴더:

store/redux/store.js
store/redux/favorites.js

store:

const store = configureStore({
  reducer: {
    favoriteMeals: favoritesReducer,
  },
});

앱에 Provider 적용:

<Provider store={store}>
  <NavigationContainer>
    ...
  </NavigationContainer>
</Provider>

favorite slice:

const favoritesSlice = createSlice({
  name: 'favorites',
  initialState: {
    ids: [],
  },
  reducers: {
    addFavorite: (state, action) => {
      state.ids.push(action.payload.id);
    },
    removeFavorite: (state, action) => {
      const index = state.ids.indexOf(action.payload.id);
      state.ids.splice(index, 1);
    },
  },
});

actions export:

export const addFavorite = favoritesSlice.actions.addFavorite;
export const removeFavorite = favoritesSlice.actions.removeFavorite;
export default favoritesSlice.reducer;

Redux 상태 읽기

const favoriteMealIds = useSelector((state) =>
  state.favoriteMeals.ids
);

Redux action dispatch

const dispatch = useDispatch();

dispatch(addFavorite({ id: mealId }));
dispatch(removeFavorite({ id: mealId }));

Context와 Redux 비교

항목Context APIRedux Toolkit
설치React 내장별도 설치 필요
구조Context + Provider + useStateStore + Slice + Provider
상태 변경직접 만든 함수 호출action dispatch
읽기useContextuseSelector
변경context의 함수useDispatch
적합한 경우간단한 공유 상태상태 규모가 크고 액션 흐름이 명확해야 하는 앱

섹션 7 체크리스트

  • route params와 app-wide state의 차이를 설명할 수 있다.
  • Context Provider를 어디에 배치해야 하는지 판단할 수 있다.
  • Context에서 상태와 상태 변경 함수를 함께 공급할 수 있다.
  • 즐겨찾기 id 배열로 원본 데이터를 필터링할 수 있다.
  • 목록 UI를 MealsList로 추출해 재사용할 수 있다.
  • Redux Toolkit의 configureStore, createSlice, Provider 흐름을 이해한다.
  • useSelector로 상태를 읽고 useDispatch로 action을 보낼 수 있다.

전체 기술 패턴 요약

1. 컴포넌트와 렌더링

HTML 태그
-> React Native 핵심 컴포넌트

CSS 파일
-> StyleSheet 객체

DOM 이벤트
-> onPress / onChangeText 등 네이티브 이벤트

2. 레이아웃

기본 레이아웃
-> View + Flexbox

제한된 스크롤
-> ScrollView

긴 목록
-> FlatList

안전 영역
-> SafeAreaView

키보드 회피
-> KeyboardAvoidingView

3. UI 피드백

Android 눌림
-> android_ripple

iOS 눌림
-> Pressable pressed style

아이콘
-> @expo/vector-icons

상태바
-> expo-status-bar

4. 반응형과 플랫폼

작은 화면
-> width / maxWidth / 조건부 스타일

회전 대응
-> useWindowDimensions

가로/세로 구조 변경
-> 조건부 JSX

플랫폼 차이
-> Platform.OS / Platform.select / .ios.js / .android.js

5. 내비게이션

앞으로/뒤로 이동
-> Native Stack

큰 섹션 전환
-> Drawer

주요 화면 탭 전환
-> Bottom Tabs

화면 데이터 전달
-> route.params

헤더 설정
-> options / screenOptions / setOptions

6. 전역 상태

간단한 공유 상태
-> Context API

명시적 액션 기반 상태
-> Redux Toolkit

화면 열기용 식별자
-> route params

여러 화면이 공유하는 변화하는 데이터
-> global state

실무 관점에서 특히 중요한 판단 기준

ScrollView와 FlatList

상황선택
짧고 고정된 콘텐츠ScrollView
길어질 수 있는 동적 목록FlatList
상세 페이지 전체 스크롤ScrollView
검색 결과/상품 목록/로그FlatList

Dimensions와 useWindowDimensions

상황선택
앱 시작 시점 크기만 필요Dimensions.get()
화면 회전/크기 변경에 반응useWindowDimensions()
스타일 객체를 파일 밖에서 고정Dimensions.get() 가능
컴포넌트 렌더링마다 최신 크기 필요useWindowDimensions()

Params와 전역 상태

데이터어디에 둘까?
mealIdroute.params
categoryIdroute.params
즐겨찾기 목록Context/Redux
로그인 사용자Context/Redux
장바구니Redux 또는 Context
헤더 제목options 또는 setOptions

Context와 Redux

상황추천
작은 앱, 단순 상태Context
여러 화면에서 몇 개 값 공유Context
상태 변경 종류가 많음Redux Toolkit
디버깅 가능한 action 흐름 필요Redux Toolkit
팀에서 상태 구조를 명확히 관리해야 함Redux Toolkit

최종 복습 질문

  1. React Native가 react-dom 대신 사용하는 렌더링 대상은 무엇인가?
  2. View 안에 문자열을 바로 넣으면 왜 문제가 되는가?
  3. RN에서 onClick 대신 어떤 이벤트를 사용하는가?
  4. ScrollViewFlatList는 언제 구분해서 써야 하는가?
  5. RN Flexbox의 기본 flexDirection은 웹과 어떻게 다른가?
  6. Expo 개발자 메뉴는 어떻게 열 수 있는가?
  7. 상태 기반 화면 전환과 React Navigation의 차이는 무엇인가?
  8. Pressablepressed 값은 어떤 UI 피드백에 유용한가?
  9. 원격 이미지를 렌더링할 때 왜 width/height가 필요한가?
  10. Dimensions.get()useWindowDimensions()의 차이는 무엇인가?
  11. 가로 모드에서는 왜 스타일뿐 아니라 JSX 구조도 바꿀 수 있는가?
  12. Platform.select()는 어떤 상황에서 쓰는가?
  13. NavigationContainer는 앱에서 어떤 역할을 하는가?
  14. navigation.navigate()의 두 번째 인자는 무엇인가?
  15. route.params는 어떤 데이터에 적합한가?
  16. screenOptionsoptions의 우선순위는 어떻게 되는가?
  17. 동적 헤더 제목 설정에 useLayoutEffect를 쓰는 이유는 무엇인가?
  18. Stack, Drawer, Bottom Tabs는 각각 어떤 내비게이션 패턴인가?
  19. nested navigator에서 header가 두 개 생기면 어떻게 해결하는가?
  20. Context는 상태 저장소인가, 전달 통로인가?
  21. Redux Toolkit의 createSlice는 무엇을 함께 정의하는가?
  22. useSelectoruseDispatch의 역할은 무엇인가?
  23. favorite meal id 배열로 실제 meal 목록을 어떻게 구하는가?
  24. MealsList처럼 화면 로직에서 목록 UI를 분리하면 어떤 장점이 있는가?

한 문장 결론

React Native 학습의 핵심은 React 문법을 다시 배우는 것이 아니라, 이미 알고 있는 React 사고방식을 모바일 네이티브 환경의 컴포넌트, 스타일, 터치, 화면 크기, 내비게이션, 전역 상태 관리 방식에 맞게 재배치하는 것이다.