본문 바로가기

cs/react

[React] 컴포넌트 생명주기

목차

1. 마운트 & 업데이트 & 언마운트

2. 컴포넌트 생명주기


마운트 & 업데이트 & 언마운트

컴포넌트의 생명주기는 크게 생성, 업데이트, 삭제이렇게 세가지로 정의할 수 있습니다.

컴포넌트 환경에서는 이 단어를 마운트, 업데이트, 언마운트로 정의하고 있습니다.

 

마운트

렌더링된 컴포넌트가 실제 DOM에 최초 커밋(적용)되는 것

https://hoazzinews.tistory.com/32

업데이트

마운트 된 이후 컴포넌트의 상태가 변경되어 리렌더링되고 실제 DOM에 수정되어 커밋되는 것

https://hoazzinews.tistory.com/32

언마운트

마운트된 컴포넌트가 실제 DOM에서 제거되는 것

https://hoazzinews.tistory.com/32


컴포넌트 생명주기

컴포넌트 생명주기를 알면 useEffect를 포함한 다른 훅(useMemo, useCallback ...) 들의 호출 시점을 한 눈에 확인할 수 있습니다.

 

https://wavez.github.io/react-hooks-lifecycle/

컴포넌트의 생명주기 흐름를 설명하기 앞서, 생명주기에는 단계라는 추상적인 개념 렌더링 단계(render phase), 커밋 단계(commit phase), 초기화 단계(cleanup phase)가 있습니다.

 

  • render phase
    • react가 컴포넌트를 호출하여 함수형 컴포넌트가 호출되고 내부 함수가 실행되며, 변수를 초기화 하고 JSX를 반환하는 단계
    • useMemo, useCallback 등의 대부분의 훅이 호출됩니다.
  • commit phase
    • 변경된 컴포넌트를 실제 DOM으로 업데이트하고 브라우저에 출력하는 단계
    • 실제 DOM 생성과 브라우저에 출력하는 그 사이에 useEffect 콜 백이 호출됩니다.
  • cleanup phase 
    • 새로운 useEffect를 호출하기 위해 이전 useEffect의 작업을 초기화하는 단계
    • 수정된 컴포넌트의 useEffect가 호출되기 전에 호출됩니다.

 

생명주기 순서

리액트 컴포넌트의 사이클은 이와 같은 순서를 따릅니다.

생성(mount) → 업데이트(update) → 제거(unmount)

 

  1. mount
    1. render phase
      • 컴포넌트 함수 호출
      • useMemo, useCallback 실행
      • 컴포넌트 return (JSX)
    2. commit phase
      • 실제 DOM 생성
      • useEffect 실행
      • 브라우저 출력
  2. update
    1. render phase
      • 컴포넌트 함수 다시 호출
      • useMemo, useCallback 실행
      • 컴포넌트 return (새로운JSX 생성)
    2. cleanup phase
      • 이전 useEffect의 clean up 실행으로 초기화
    3. commit phase
      • 수정된 컴포넌트를 실제 DOM에 적용
      • useEffect 실행
      • 수정된 브라우저 출력
  3. unmount
    • 컴포넌트가 실제 DOM에서 삭제
    • 컴포넌트의 모든 useEffect의 clean up 실행

 

코드 레벨의 컴포넌트 생명주기

예시는 부모는 자식을 포함하고 있고 부모에서 상태 변경으로 렌더링을 실행 하도록 구성하였습니다.

function Parents(props) {
    const [data, setData] = useState(false)

    useEffect(() => {
        console.log("부모 컴포넌트 마운트 후");
    }, [])

    useEffect(() => {
        console.log("부모 컴포넌트 (리)렌더링 후");
    })

    console.log("부모 컴포넌트 (리)렌더링 전");

    function handleParentsState() {
        console.log("부모 컴포넌트 함수 호출");
        setData(!data);
    }

    return (
        <div>
            <Childs/>
            <button onClick={handleParentsState}>부모 상태 변경</button>
        </div>
    )
}
function Childs(props) {
    useEffect(() => {
        console.log("자식 컴포넌트 마운트 후");
    },[])

    useEffect(() => {
        console.log("자식 컴포넌트 (리)렌더링 후")
    })

    console.log("자식 컴포넌트 (리)렌더링 전");
}

코드를 실행하면 아래와 같은 결과가 나오게 됩니다.

마운트 직후

 

부모의 상태 변경 직후

  1. 부모 컴포넌트 (리)렌더링 전
    • 부모 컴포넌트 생명주기 mount의 render phase 시점
    • 부모 컴포넌트의 함수와 변수 초기화를 진행하면서 호출
  2. 자식 컴포넌트 (리)렌더링 전
    • 자식 컴포넌트 생명주기 mount의 render phase 시점
    • 자식 컴포넌트의 함수와 변수 초기화를 진행하면서 호출
  3. 자식 컴포넌트 마운트 후
    • 자식 컴포넌트 생명주기 mount에서 commit phase 직후 시점
    • 자식이 실제 DOM에 삽입되고 브라우저에 출력 된 후에 호출
    • 컴포넌트 인스턴스(fiber)가 새로 생성되지 않는 이상 단 한번만 호출
  4. 자식 컴포넌트 (리)렌더링 후
    • 자식 컴포넌트 생명주기 mount에서 commit phase 직후 시점
    • 3번 호출되는 시점과 동일.
    • 자식이 렌더링 될때마다 호출
  5. 부모 컴포넌트 마운트 후
    • 부모 컴포넌트 생명주기 mount에서 commit phase  직후 시점
    • 부모가 실제 DOM에 삽입되고 브라우저에 출력 된 후에 호출
    • 컴포넌트 인스턴스(fiber)가 새로 생성되지 않는 이상 단 한번만 호출
  6. 부모 컴포넌트 (리)렌더링 후
    • 부모 컴포넌트 생명주기 mount에서 commit phase 직후 시점
    • 5번 호출되는 시점과 동일
    • 부모가 렌더링 될때마다 호출
  7. 부모 컴포넌트 함수 호출
    • 부모 컴포넌트 내부에 정의된 상태 변경 이벤트 발생
    • 생명주기 update를 발생시킴
  8. 부모 컴포넌트 (리)렌더링 전
    • 부모 컴포넌트 생명주기 update에서 render phase 시점
    • 부모 컴포넌트의 변경된 상태를 초기화 하는 시점에 호출
  9. 자식 컴포넌트 (리)렌더링 전
    • 자식 컴포넌트 생명주기 update에서 render phase 시점
    • 자식 컴포넌트의 함수 호출, 변수 초기화 하는 시점에 호출
  10. 자식 컴포넌트 (리)렌더링 후
    • 자식 컴포넌트 생명주기 update에서 commit phase 직후 시점
    • 자식이 렌더링 될때마다 호출
  11. 부모 컴포넌트 (리)렌더링 후
    • 부모 컴포넌트 생명주기 update에서 commit phase 직후 시점
    • 부모가 렌더링 될때마다 호출

코드레벨 생명주기 - cleanup phase

부모가 자식을 mount하고 update하고 unmount를 제어하는 과정을 코드 레벨로 알아보겠습니다.

 

(컴포넌트의 생명주기에서 render phase, commit phase와 분리해서 정리하는 이유는.. 세개 다 한번에 이해하려니까 제가 헷갈려서 따로 분석해서 그렇습니다..)

 

부모가 자식을 제외하는 이벤트와 자식이 unmount시 모든 clean up을 호출하는지 확인하기 위해 변경되지 않는 상태(test)를 의존하는useEffectt를 추가했습니다.

function Parents(props) {
    const [data, setData] = useState(false)
    const [visible, setVisible] = useState(true);
    const visibleChildBtnTitle = "자식" + (visible ? "숨기기" : "보이기")

    //부모 마운트 시점 1번 실행
    useEffect(() => {
        console.log("부모 컴포넌트 마운트 후");

        return () => {
            console.log("부모 컴포넌트 실제 DOM 제외");
        }
    }, [])

    //부모 렌더링 할때마다 실행
    useEffect(() => {
        console.log("부모 컴포넌트 (리)렌더링 후");

        return () => {
            console.log("부모 컴포넌트 (리)렌더링 clean up");
        }
    })

    //부모 render phase 시점 실행
    console.log("부모 컴포넌트 (리)렌더링 전");

    //부모의 상태 변경으로 리렌더링 실행용
    function handleParentsState() {
        console.log("부모 컴포넌트 함수 호출");
        setData(!data);
    }

    //자식 unmount 시키는용
    function handleChildVisibleState() {
        setVisible(!visible);
    }

    return (
        <div>
            {visible && <Childs/>}
            <button onClick={handleParentsState}>부모 상태 변경</button>
            <button onClick={handleChildVisibleState}>{visibleChildBtnTitle}</button>
        </div>
    )
}
function Childs(props) {
    //clean up 테스트용
    const [test,setTest] = useState(null);

    //자식 마운트 시점 1번 실행
    useEffect(() => {
        console.log("자식 컴포넌트 마운트 후");

        return () => {
            console.log("자식 컴포넌트 실제 DOM 제외");
        }
    },[])

    //자식 렌더링 할때마다 실행
    useEffect(() => {
        console.log("자식 컴포넌트 (리)렌더링 후")

        return () => {
            console.log("자식 컴포넌트 (리)렌더링 clean up");
        }
    })

    //unmount시 모든 clean up 발생 여부 테스트용
    useEffect(() => {
        console.log("자식 컴포넌트 test");

        return () => {
            console.log("자식 컴포넌트 test Cleanup");
        }
    },[test])

    //부모 render phase 시점 실행
    console.log("자식 컴포넌트 (리)렌더링 전");
}

이미지 1. 부모, 자식 마운트 시
이미지 2. 부모 상태 변경으로 인한 리렌더링 시

결과 분석(mount, update)

  • 의존성 배열을 정의하지 않은 useEffect의 cleanup phase가 자식 → 부모 순으로 호출 (이미지 2 참고)
  • 새명주기 update시 render phase → cleanup phase → commit phase(이후 호출되는 useEffect) 순으로 호출
    [부모 컴포넌트 기준]
    1. 부모 컴포넌트 (리)렌더링 전 (render phase)
    2. 부모 컴포넌트 (리)렌더링 clean up (cleanup phase)
    3. 부모 컴포넌트 (리)렌더링 후 (commit phase 이후)
  • test를 의존하는 useEffect는 마운트 시에 한 번만 호출 (이미지 1 참고)

 

이미지 3. 부모에서 자식 삭제

결과 분석(unmount)

  • 의존성 배열에 빈배열을 정의한 useEffect의 clean up이 호출되고 나머지 useEffect(렌더링 마다 호출되는 것, test 상태를 의존하는 것)의 clean up이 전부 호출
  • 부모에서 자식은 삭제되었기 때문에 cleanup phase 이후 useEffect는 호출되지 않음

컴포넌트 생명주기를 통해 컴포넌트의 각 훅들의 호출 시점과 자식과 부모간의 생명주기 관계를 알 수 있었습니다.

 

재조정 흐름(생명주기 update)에서 부모가 리렌더링 되었을 때 변경사항을 찾으면 fiber를 생성하거나 update태그를 추가합니다.그리고 해당 컴포넌트의 자식을 호출해서 렌더링 시키는 걸 재귀적으로 수행한다고 했는데, 부모가 render phase에서 commit phase로 가지 않고 자식을 먼저 생명주기를 태운 후 마지막에 부모가 commit phase로 가는걸 보고 어떻게 재귀가 흘러가는지 이해할 수 있게 되었습니다.


참고

https://hoazzinews.tistory.com/32

 

리액트 컴포넌트 마운트란?

지난 시간에는 '렌더링'에 대해서 살펴봤습니다. 리액트 컴포넌트 렌더링이란?함수형 컴포넌트를 코딩할 때 JSX로 만듭니다. 이렇게 만들어진 컴포넌트는 프로그램이 실행되면 가상 돔(Virtual Dom

hoazzinews.tistory.com

 

'cs > react' 카테고리의 다른 글

[React] Redux Toolkit  (0) 2025.12.01
[React] Context API & Provider 구조  (0) 2025.11.29
[React] 리액트 훅  (0) 2025.11.27
[React] 가상 DOM과 재조정  (0) 2025.11.24
[React] react와 useState의 동작 원리  (0) 2025.11.21