Skip to main content
네이티브
입문네이티브React Native 플랫폼 · 8

React 컴포넌트와 앱 생명주기: 화면이 남은 채 앱이 뒤로 갈 수 있는가

컴포넌트 제거와 앱의 전경·배경 전환을 다른 사건으로 추적하고, 컴포넌트 제거 때의 정리에만 의존해 배경 전환을 놓치는 카메라 세션을 앱 상태 구독으로 교정한다.

마지막 검증 재검증 정책: 제품 버전 의존: 새 주요 버전마다 재검증
목차
표준·구현·측정·해석 표시는 무엇인가요?
  • 표준웹 표준이나 언어 명세가 정한 동작
  • 구현특정 기술이나 브라우저가 실제로 구현한 동작
  • 측정명시한 환경에서 직접 실행해 관찰한 결과
  • 해석앞선 근거에서 도출한 설계 판단
  • 미확인아직 공식 근거나 재현 결과를 확인하지 못한 내용

30초 요약

React 컴포넌트가 트리에서 제거되는 일과 모바일 앱이 화면 뒤의 background 상태로 가는 일은 다르다. AppState의 active는 사용자가 조작하는 전경, background는 화면 뒤, iOS의 inactive는 두 상태 사이 또는 일시 비활성 상태를 뜻한다.

컴포넌트 제거  → 해당 Effect의 마지막 cleanup 실행
앱 background → 컴포넌트가 남은 채 AppState만 바뀔 수 있음
프로세스 종료  → 현재 JavaScript 실행 자체가 사라지는 별도 경계

컴포넌트가 있는 동안에도 앱이 active일 때만 유지해야 하는 카메라·오디오·연결은 AppState 변화와 동기화한다. 프로세스 종료 뒤 복원은 다음 9편에서 분리한다.

이 글을 관통하는 상황: Home으로 갔는데 카메라 cleanup이 없다

가상의 배송 스캔 화면은 카메라 세션을 컴포넌트가 나타날 때 시작하고 제거될 때 멈춘다. cameraSession은 이 글의 실행 순서만 관찰하도록 startstop 호출을 기록하는 가상 어댑터다.

실제 카메라 SDK가 아니라 호출 횟수와 현재 실행 여부를 기록하는 fixture를 만든다. startCallCount·stopCallCount는 모든 호출을 세고, startCount·stopCount는 실제 실행 상태가 바뀐 횟수만 센다. 이 둘을 나눠야 외부 코드의 중복 호출도 숨지 않는다.

// camera-session-fixture.ts
let running = false
let startCallCount = 0
let stopCallCount = 0
let startCount = 0
let stopCount = 0
 
function printSnapshot(label: string) {
  console.info(label, {
    running,
    startCallCount,
    stopCallCount,
    startCount,
    stopCount,
  })
}
 
export const cameraSession = {
  start() {
    startCallCount += 1
    if (running) {
      printSnapshot('camera.start ignored')
      return
    }
    running = true
    startCount += 1
    printSnapshot('camera.start')
  },
  stop() {
    stopCallCount += 1
    if (!running) {
      printSnapshot('camera.stop ignored')
      return
    }
    running = false
    stopCount += 1
    printSnapshot('camera.stop')
  },
  snapshot(label: string) {
    printSnapshot(label)
    return {
      running,
      startCallCount,
      stopCallCount,
      startCount,
      stopCount,
    }
  },
}
import { useEffect, useState } from 'react'
import { Pressable, Text, View } from 'react-native'
import { cameraSession } from './camera-session-fixture'
 
function Scanner() {
  const [status, setStatus] = useState('멈춤')
 
  useEffect(() => {
    cameraSession.start()
    setStatus('실행 중')
 
    return () => {
      cameraSession.stop()
    }
  }, [])
 
  return <Text>카메라 세션: {status}</Text>
}
 
export default function App() {
  const [showScanner, setShowScanner] = useState(true)
 
  return (
    <View style={{ padding: 24 }}>
      <Pressable onPress={() => setShowScanner(value => !value)}>
        <Text>{showScanner ? '스캔 화면 제거' : '스캔 화면 복원'}</Text>
      </Pressable>
      <Pressable onPress={() => cameraSession.snapshot('수동 기준값')}>
        <Text>fixture 기준값 기록</Text>
      </Pressable>
      {showScanner ? <Scanner /> : null}
    </View>
  )
}

fixture 기록을 보기 전에 예측한다.

  1. 스캔 화면 제거를 눌러 Scanner가 React 트리에서 사라지면 cleanup은 실행되는가?
  2. Scanner를 다시 보이게 한 뒤 기기의 Home으로 가면 React 트리에서도 제거되는가?
  3. background 전환을 구독하지 않은 코드가 cameraSession.stop()을 호출할 이유가 있는가?

개발 Strict Mode에서는 Effect 연결이 안전한지 점검하려고 첫 setup 뒤 cleanup과 setup을 한 번 더 실행할 수 있다. 따라서 앱이 처음 안정된 뒤 fixture 기준값 기록을 눌러 기준값을 남기고, 아래처럼 그 사건 전후의 증가량을 본다. 초기 절대 횟수는 Home 사건의 증거로 쓰지 않는다.

  1. 앱을 완전히 새로 열고 화면이 안정되면 Home 직전 기준값을 기록한다.
  2. 스캔 화면 제거 전후에는 stopCallCountstopCount가 각각 1 증가하는지 확인한다.
  3. 화면을 복원하고 새 Home 직전 기준값을 기록한 뒤 기기의 Home으로 간다.
  4. 앱으로 돌아와 fixture 기준값 기록을 다시 누르고 Home 전후 증가량을 비교한다.

아래는 미리 준비한 가상 증거인 fixture 기록이다. React Native의 고정 로그가 아니다.

앱의 첫 Effect 점검           기준값을 잡기 전이므로 절대 횟수는 무시
스캔 화면 제거               stopCallCount +1, stopCount +1
스캔 화면 복원               startCallCount +1, startCount +1
Home 직전                    running=true 기준값 기록
기기 Home                    AppState active → background
React Scanner unmount         없음
camera.stop 추가 호출         없음, stopCallCount +0, stopCount +0
앱 다시 열기                 AppState background → active
복귀 snapshot                running=true, Home 전후 stopCallCount +0

컴포넌트 제거에서는 cleanup이 실행됐다. 하지만 Home 전환에서는 Scanner가 트리에 남아 cleanup이 없었다. 문제는 React cleanup 실패가 아니라 앱 상태 변화를 외부 세션 조건에 넣지 않은 것이다.

background는 React unmount가 아니다

앱 상태를 외부 세션과 동기화한다

카메라 계약을 “컴포넌트가 존재하고 AppState가 active일 때만 실행”으로 바꾼다. 첫 번째 예제의 App.tsx 전체를 다음 코드로 교체한다.

import { useEffect, useState } from 'react'
import {
  AppState,
  type AppStateStatus,
  Pressable,
  Text,
  View,
} from 'react-native'
import { cameraSession } from './camera-session-fixture'
 
function Scanner() {
  const [status, setStatus] = useState('멈춤')
 
  useEffect(() => {
    let running = false
 
    function syncSession(nextState: AppStateStatus) {
      if (nextState === 'active' && !running) {
        cameraSession.start()
        running = true
        setStatus('실행 중')
        return
      }
 
      if (nextState !== 'active' && running) {
        cameraSession.stop()
        running = false
        setStatus('일시 중지')
      }
    }
 
    const subscription = AppState.addEventListener('change', syncSession)
 
    try {
      syncSession(AppState.currentState)
    } catch (error) {
      subscription.remove()
      throw error
    }
 
    return () => {
      subscription.remove()
      if (running) cameraSession.stop()
    }
  }, [])
 
  return <Text>카메라 세션: {status}</Text>
}
 
export default function App() {
  const [showScanner, setShowScanner] = useState(true)
 
  return (
    <View style={{ padding: 24 }}>
      <Pressable onPress={() => setShowScanner(value => !value)}>
        <Text>{showScanner ? '스캔 화면 제거' : '스캔 화면 복원'}</Text>
      </Pressable>
      <Pressable onPress={() => cameraSession.snapshot('수동 기준값')}>
        <Text>fixture 기준값 기록</Text>
      </Pressable>
      {showScanner ? <Scanner /> : null}
    </View>
  )
}

running은 이 Effect 연결 하나가 카메라를 실제 시작했는지 기억해 중복 start·stop을 막는다. 컴포넌트가 제거되면 해당 Effect도 끝나므로 컴포넌트 상태를 cleanup에서 다시 바꿀 필요는 없다. 구독을 먼저 등록하고 현재 상태를 읽어 동기화하면 두 작업 사이의 앱 상태 전환을 놓치지 않는다. 초기 동기화가 실패하면 방금 만든 구독도 즉시 제거한다.

교정 코드로 바꾼 뒤에는 Fast Refresh나 Home 복귀에 의존하지 않는다. 개발 메뉴의 Reload로 JavaScript를 다시 불러오거나 앱 프로세스를 종료한 뒤 재실행해 fixture가 기억한 상태를 초기화한다. 화면이 안정된 시점에 fixture 기준값 기록을 눌러 새 기준값을 잡고 다음 증가량을 확인한다.

앱의 첫 Effect 점검           기준값을 잡기 전이므로 절대 횟수는 무시
안정된 Scanner               AppState active, running=true 기준값 기록
기기 Home                    AppState background, stopCallCount +1, stopCount +1, running=false
React Scanner unmount         없음
앱 다시 열기                 AppState active, startCallCount +1, startCount +1, running=true
스캔 화면 제거               subscription.remove, stopCallCount +1, stopCount +1, running=false
제거 뒤 AppState 전환         startCallCount +0, stopCallCount +0

성공 조건은 cleanup 횟수만 세는 것이 아니다. 앱이 active가 아닌 모든 기록 구간에서 카메라 세션이 멈춰 있어야 한다. 각 Home·복귀·제거 사건에서 대응하는 호출 횟수도 정확히 1 증가하고, 컴포넌트 제거 뒤 AppState 사건에서는 두 호출 횟수가 모두 0 증가해야 한다.

플랫폼별 추가 상태를 제품 조건으로 결정한다

반대로 iOS inactive를 단순 background와 다르게 처리해야 하는 기능도 있을 수 있다. 이 글의 카메라 계약은 보수적으로 active가 아닌 모든 상태에서 멈춘다. 오디오 재생이나 통화처럼 background 실행 정책이 필요한 기능은 같은 규칙을 복사하지 말고 해당 운영체제 기능 계약을 확인한다.

가상 기록과 실제 카메라 검증을 구분한다

Android Emulator와 iOS Simulator에서는 먼저 Home 이동과 앱 복귀를 실행해 AppState 동기화 흐름을 확인한다. 가상 환경이 해당 카메라 입력을 제공하고 검증 목적에 충분한지도 따로 확인한다. 실제 하드웨어 자원이나 기기 중단 상황이 합격 조건이라면, 사용하는 카메라 SDK를 연결한 지원 실기기에서 같은 순서를 다시 실행하고 권한·미리보기·자원 재획득 결과까지 관찰한다.

앱 상태와 프로세스 종료를 합치지 않는다

이 글에서는 active→background→active를 한 프로세스 안에서 관찰한다. background 중 프로세스가 사라져 cleanup을 실행할 기회조차 없을 수 있는 상황은 다음 9편에서 별도 질문으로 다룬다.

같은 상황을 다시 진단한다

문제       Home 이동 뒤에도 카메라 stop 기록이 없고 복귀 화면은 실행 중으로 남음
잘못된 판단 background가 되면 React가 모든 컴포넌트를 unmount하고 cleanup을 실행함
관찰       AppState만 active→background로 바뀌고 Scanner unmount는 없음
원인       카메라 세션 조건을 컴포넌트 존재 하나로만 정함
행동       AppState change를 구독해 active에서 시작하고 나머지 상태에서 중지
검증       background 중 중지, active 복귀 한 번 시작, unmount 뒤 구독·세션 해제

풀 리퀘스트에서는 다음을 확인한다.

  • React 컴포넌트 제거와 운영체제 앱 상태 전환을 같은 사건으로 취급하지 않았는가?
  • 외부 세션이 필요한 조건에 컴포넌트 존재와 AppState를 모두 반영했는가?
  • AppState 구독과 외부 세션을 Effect cleanup에서 함께 해제하는가?
  • iOS inactive와 Android blur를 제품 요구에 맞게 분리했는가?
  • 개발 Strict Mode의 추가 setup·cleanup을 실제 앱 상태 사건으로 세지 않았는가?
  • 프로세스 종료 뒤 복원을 AppState 처리로 대신하지 않았는가?

기억할 문장은 짧다.

컴포넌트가 남아 있어도 앱은 background일 수 있다. 외부 세션은 두 생명주기를 모두 따른다.

다음 글에서는 운영체제가 앱 프로세스를 종료한 뒤 무엇을 저장하고 어떻게 다시 시작해야 하는지 살펴본다.

Active recall

기억에서 꺼내 보기

답을 쓰지 않아도 됩니다. 먼저 머릿속으로 답한 뒤 펼쳐서 정답·이유·흔한 오해를 비교하세요.

  1. 01사용자가 Home으로 가 앱이 background가 되면 현재 React 화면의 모든 컴포넌트 cleanup도 반드시 실행될까?

    정답

    아니다. 앱 상태 전환과 React 트리에서 컴포넌트가 제거되는 일은 별개다. 컴포넌트가 남아 있으면 unmount cleanup은 실행되지 않을 수 있다.

    왜 그런가

    앱 상태에 따라 멈춰야 하는 외부 자원을 unmount cleanup에만 맡기면 background 전환을 놓친다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  2. 02카메라 세션이 컴포넌트가 있는 동안에도 앱이 active가 아닐 때 멈춰야 한다면 무엇을 구독해야 할까?

    정답

    AppState change를 구독해 active일 때 시작하고 background·iOS inactive일 때 멈춘다.

    왜 그런가

    컴포넌트 존재 조건과 앱 활성 조건을 모두 외부 세션 계약에 반영해야 한다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  3. 03Android 알림 창을 내려 사용자가 앱을 조작하지 못하면 AppState도 반드시 background로 바뀔까?

    정답

    아니다. AppState는 그대로일 수 있고 별도 blur 사건이 발생할 수 있다. 제품이 이 상태까지 멈춤 조건으로 삼는지 따로 결정한다.

    왜 그런가

    foreground/background와 사용자 초점 상실은 Android에서 같은 신호가 아닐 수 있다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

  4. 04AppState가 background에서 active로 돌아온 기록만으로 앱 프로세스가 계속 살아 있었다고 항상 증명할 수 있을까?

    정답

    현재 실행의 상태 전환은 알 수 있지만, 운영체제가 프로세스를 종료한 뒤 새 실행이 시작되는 복원 경계까지 AppState 하나로 설명할 수는 없다.

    왜 그런가

    background 전환 처리와 프로세스 종료 뒤 저장·복원은 별도 설계다.

    관련 설명 다시 읽기
    지금 어느 정도 기억났나요?

    선택한 상태와 다음 복습일은 이 브라우저에만 저장됩니다.

출처와 검증 범위

아래 날짜는 링크를 마지막으로 열어 본 날입니다. 문서 상단의 검증일은 글의 설명과 적용 범위를 다시 확인한 날입니다.

  1. AppStateReact Native · 공식 문서 · 확인 2026-08-09
  2. useEffectReact · 공식 문서 · 확인 2026-08-09
  3. Lifecycle of Reactive EffectsReact · 공식 문서 · 확인 2026-08-09
  4. The activity lifecycleAndroid Developers · 공식 문서 · 확인 2026-08-09
  5. Managing your app's life cycleApple Developer · 공식 문서 · 확인 2026-08-09
  6. Extended controls, settings, and helpAndroid Developers · 공식 문서 · 확인 2026-08-09
  7. Running your app on simulated or physical devicesApple Developer · 공식 문서 · 확인 2026-08-09
  8. Debugging BasicsReact Native · 공식 문서 · 확인 2026-08-09
이 문서의 마지막까지 읽었습니다.