React Native의 라이프 사이클

eeensu·2026년 2월 18일

React Native

목록 보기
11/35

RN 의 라이프 사이클은, React 와 동일하게 운영될까? 마운트, 언마운트, 리마운트 말이다.

이러한 라이프 사이클은 React 든 RN이든 동일하게 운영된다. useState, useEffect 같은 훅들이 돌아가는 시점이나 마운트, 업데이트, 언마운트의 개념은 완전히 같다. RN 도 결국 React 라이브러리를 그대로 쓰기 때문이다.

하지만 모바일 앱(App) 환경이기 때문에 추가되는 "앱 라이프사이클(App Lifecycle)""화면 네비게이션 라이프사이클"이 존재한다. 이 부분이 웹과 다르다.


1. 컴포넌트 라이프사이클 (동일함)

웹에서 하던 거랑 똑같다.

  • Mount : 컴포넌트가 화면에 나타날 때 (useEffect([], ...)).
  • Update : 상태(State)나 Props가 변해서 리렌더링 될 때.
  • Unmount : 컴포넌트가 화면에서 사라질 때 (useEffect return).


2. 앱 라이프사이클 (App Lifecycle - 차이점)

웹 브라우저는 탭을 닫거나 새로고침하면 끝이지만, 모바일 앱은 "백그라운드(Background)" 상태가 있다. 사용자가 홈 버튼을 눌러서 나갔다가 다시 돌아올 수 있기 때문이다.

React Native에서는 AppState API를 통해 이를 감지한다.

  • active : 앱이 현재 화면에 떠 있고, 사용자가 조작 가능한 상태 (Foreground).
  • background : 앱이 숨겨진 상태. (홈 버튼을 눌렀거나 다른 앱으로 전환됨). 안드로이드에선 이 상태일 때 코드가 멈출 수도 있다.
  • inactive : active와 background 사이의 과도기 상태이며 ios에만 존재한다.
    (예: iOS에서 알림 센터를 내리거나, 멀티태스킹 화면에 진입했을 때.)
import React, { useEffect, useState } from 'react';
import { AppState, Text } from 'react-native';

const AppLifecycleExample = () => {
  useEffect(() => {
    // 앱 상태 변경 감지 리스너 등록
    const subscription = AppState.addEventListener('change', nextAppState => {
      if (nextAppState === 'active') {
        console.log('앱이 다시 활성화되었습니다! (Foreground)');
      } else if (nextAppState.match(/inactive|background/)) {
        console.log('앱이 백그라운드로 갔습니다.');
      }
    });

    return () => {
      subscription.remove();
    };
  }, []);

  return <Text>앱 상태 모니터링 중...</Text>;
};


3. 네비게이션 라이프사이클 (가장 큰 차이점)

웹(React Router)과 앱(React Navigation)은 화면 이동 방식이 다르다. 이게 실무에서 가장 많이 헷갈리는 부분이다.

  • 웹 (React Router): 페이지를 이동(a -> b)하면, a 컴포넌트는 Unmount(파괴)된다. useEffect의 cleanup 함수가 실행된다.

  • 앱 (Stack Navigation) : 화면을 이동(a -> b)하면, a 컴포넌트는 Unmount 되지 않고 그대로 메모리에 살아있다. (카드 덮기 방식).

따라서 b에서 뒤로 가기를 눌러 a로 돌아와도 a는 재마운트(Mount) 되지 않는다.
그냥 가려져 있던 게 다시 보이는 것뿐이다.

1.useFocusEvent

그래서 React Native에서는 화면이 다시 포커스(Focus) 되었을 때를 감지하기 위해 useFocusEffect 라는 전용 훅을 쓰며, 다음과 같은 상황이 있다.

  • 화면에 진입할 때마다 최신 데이터를 서버에서 다시 가져와야 할 때.
  • 화면을 벗어날 때 비디오 재생을 멈추거나, 카메라를 꺼야 할 때.
  • 사용자 분석(Analytics)을 위해 "이 화면을 봤음" 이벤트를 보낼 때.
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';

// 화면이 포커스 될 때마다 실행됨 (매번 실행)
useFocusEffect(
  useCallback(() => {
    console.log('이 화면이 보입니다!');
    
    return () => {
      console.log('이 화면이 사라졌습니다(다른 화면으로 덮힘 or 뒤로가기)');
    };
  }, [])
);

가장 중요한 점은 useCallback과 함께 사용해야 한다는 것이다. 그렇지 않으면 무한 루프에 빠질 수 있다.

스택에서 화면이 언마운트되지 않고 포커스만 잃는다는 점이 웹과 큰 차이점이다.

이제 성능과 직결된 라이프라이클 2가지를 더 보자.


2.useIsFocused

현재 화면이 포커스 상태인지 boolean 값으로 반환한다. 조건부 렌더링이나 특정 로직 분기에 사용한다.

import { useIsFocused } from '@react-navigation/native';
const MyScreen = () => {
  const isFocused = useIsFocused();
  return <Text>{isFocused ? '현재 이 화면 보는 중' : '다른 화면 보는 중'}</Text>;
};

다만 useIsFocused는 포커스 상태가 바뀔 때마다 리렌더링을 유발하기 때문에, 단순히 진입/이탈 시점에 뭔가를 실행하고 싶다면 useFocusEffect를 쓰는 게 더 적합하다.


4. InteractionManager

웹에서는 페이지가 뜨자마자 데이터를 불러와도(useEffect) 상관없지만, 앱은 화면 전환 애니메이션이 있어서 문제가 된다.

  • 상황 : A 화면에서 B 화면으로 넘어갈 때, 슬라이드 애니메이션이 0.5초 동안 실행된다.
  • 문제 : 근데 B 화면이 켜지자마자(Mount) 무거운 데이터 연산이나 API 호출을 하면, CPU가 바빠져서 애니메이션이 뚝뚝 끊긴다(Frame Drop).
  • 해결: "야, 애니메이션 다 끝나고 나서 이 코드 실행해."라고 명령하는 게 바로 InteractionManager다.
import React, { useEffect, useState } from 'react';
import { InteractionManager, Text, View } from 'react-native';

const ExpensiveScreen = () => {
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    // 1. 화면 전환 애니메이션이 완전히 끝날 때까지 기다린다.
    const task = InteractionManager.runAfterInteractions(() => {
      // 2. 애니메이션 끝! 이제 무거운 작업을 시작한다.
      console.log('애니메이션 종료, 데이터 로딩 시작');
      setIsReady(true);
    });

    return () => task.cancel(); // 청소(Cleanup)
  }, []);

  if (!isReady) {
    return <View><Text>로딩 중... (애니메이션은 부드럽게 돌아감)</Text></View>;
  }

  return <View><Text>엄청 무거운 데이터 화면!</Text></View>;
};


5. onLayout

웹의 CSS는 브라우저가 렌더링 하기 전에 크기를 대충 알 수 있지만, React Native는 비동기 레이아웃 시스템(Yoga Engine)을 쓴다.

  • 문제 : 컴포넌트를 그리기 전에는 width나 height를 정확히 알 수 없다.
    "이 박스의 높이 반만큼 이동하고 싶은데, 높이를 모르네?"
  • 해결 : onLayout이라는 라이프사이클 이벤트를 쓴다. 화면에 그림이 다 그려진 직후(DidUpdate)에 딱 한 번 실행되면서 정확한 크기 좌표(x, y, width, height)를 알려준다.
import React, { useState } from 'react';
import { View, Text } from 'react-native';

const LayoutExample = () => {
  const [boxHeight, setBoxHeight] = useState(0);

  return (
    <View
      // 1. 렌더링이 끝나면 이 함수가 실행된다.
      onLayout={(event) => {
        const { height } = event.nativeEvent.layout;
        console.log('내 실제 높이:', height);
        setBoxHeight(height);
      }}
      style={{ padding: 20, backgroundColor: 'red' }}
    >
      <Text>
        글자 양에 따라 높이가 변하는 박스.
        React Native는 이 박스가 다 그려지기 전까진 높이를 모름
      </Text>
    </View>
  );
};

하지만, onLayout은 렌더링 후에 실행되므로, 여기서 setState를 하면 필수로 리렌더링(Re-render)이 일어난다. 즉, 화면이 한 번 깜빡일 수 있으니 꼭 필요할 때만 써야 한다.


모바일 앱에서는 화면이 "사라진다"고 해서 항상 언마운트되는 게 아니다. Stack Navigator 기준으로는 스택에 쌓인 채 대기하는 경우가 대부분이다. 그래서 데이터 새로고침, 구독 재등록 같이 "화면에 돌아올 때마다 실행해야 하는 작업"은 useEffect 대신 useFocusEffect를 써야 한다는 걸 기억해두자.

profile
안녕하세요! 프론트엔드 개발자입니다! (2024/03 ~)

0개의 댓글