[React] 컴포넌트 생명주기(라이프 사이클)

Sohyerim·2025년 12월 12일

React

목록 보기
1/3
post-thumbnail

리액트를 처음 배우기 시작하면, 코드가 언제 실행이 되는 건지, 렌더링은 왜 여러 번 일어나는 건지 이해하기가 어렵게 느껴지기도 합니다. 그래서 리액트 컴포넌트의 생성되고, 업데이트되고, 사라지기까지의 흐름을 정리해 보겠습니다.

컴포넌트 생명주기
(이미지 출처 - 멋쟁이사자처럼 웹 프론트엔드 부트캠프 15기, GD쌤 강의자료)

컴포넌트의 생명주기는 마운팅, 업데이팅, 언마운팅으로 세 부분으로 나눠볼 수 있습니다.


1.마운팅(mounting)

  • 컴포넌트가 처음으로 DOM에 추가되고 렌더링 되는 단계를 말합니다.
  • 상태(state)와 속성(props)이 초기화되는 단계입니다.

1-1 constructor()

  • 컴포넌트 인스턴스가 생성될 때 한 번 호출됩니다.
  • super(props)를 호출하지 않으면 this를 사용하지 못하므로 this.state, this.props 사용이 불가합니다.
  • props를 기반으로 상태를 초기화하는 코드를 작성합니다.
  • props를 기반으로 상태를 초기화할 필요가 없으면 생성자를 작성할 필요가 없습니다.

→ 처음에 이 설명을 듣고 무슨 말인지 잘 이해가 가지 않았습니다. 이 과정은 클래스 컴포넌트의 경우에 해당이 됩니다. 클래스 컴포넌트는 React.Component의 상속을 받아 만들어지는데, 그러므로 클래스 컴포넌트의 부모 클래스는 React.Component인 것입니다. super(props)를 호출한다는 것은 부모 클래스인 React.Component의 생성자를 먼저 실행하겠다는 뜻입니다. 이 과정을 통해 React는 this를 초기화하고, this.props를 사용할 수 있도록 준비합니다.

  • constructor에서 주로 하는 일은 props를 기반으로 초기 state를 만드는 것입니다. 이 단계에서 설정한 state는 이후 렌더링의 기준이 됩니다.

하지만, 많은 경우 constructor를 직접 만들지 않아도 됩니다. 왜냐하면, state를 props로부터 만들 필요가 없거나 기본값만 있으면 되는 경우가 대부분이기 때문입니다. 예를 들어,

class Counter extends React.Component {
  state = {
    count: 0,
  };
}

위 예시처럼 클래스 필드 문법을 사용하면 리액트가 내부적으로 초기 state를 설정해주기 때문에, constructor를 직접 작성할 필요가 없어집니다.

그리고 중요한 점은 최근에 함수형 컴포넌트와 Hooks가 표준이 되면서, constructor를 직접 사용할 일은 점점 줄어들고 있다는 것입니다.

2-1 static getDerivedStateFromProps(props, state)

  • 부모 컴포넌트로부터 전달받은 props를 기반으로 상태를 업데이트 하고 싶을 때 사용합니다.
  • props나 state 값에 의해서 업데이트되는 새로운 state를 리턴하도록 작성합니다.
  • 일반적으로 props가 state에 영향을 주는 경우가 많지 않기 때문에 사용할 일은 거의 없습니다.

+) getDerivedStateFromProps는 부모로부터 받은 props 변화에 따라 state를 ‘동기화’해야 할 때 사용하는 생명주기 메서드입니다. 이 메서드는 언제 호출이 될까요?

  • 컴포넌트가 처음 마운트될 때
  • 부모로부터 새로운 props를 받을 때
  • state가 변경되어 다시 렌더링될 때

즉, 렌더링 직전에 항상 호출될 수 있는 단계입니다.
그러므로 이 메서드는 side effect를 만들면 안 되고, 순수하게 값 계산만 해야 합니다.

그렇다면 side effect(부작용)란 무엇일까요?

  • side effect는 “함수를 실행했을 때, 값 계산 말고 바깥에 영향을 주는 모든 행동”을 말합니다.

이 설명만으론 아직 좀 이해가 어렵습니다.
그렇다면, side effect가 있는 경우 혹은 해야하는 경우로 어떤 것이 있을까요?

side effect가 있는 경우

  1. 외부 상태나 전역 값의 변경 (변수 재할당)
  2. 서버 통신
  3. DOM 조작
  4. 타이머, 이벤트 등록
  5. 로그 출력 (엄밀히 말하면 side effect이지만 디버깅 목적으로는 허용되는 경우가 많습니다)

side effect를 해야하는 곳

  1. componentDidMount
  2. componentDidUpdate
  3. componentWillUnmount
  4. 함수형 컴포넌트의 useEffect

getDerivedStateFromProps 메서드는 잘못 사용하면 state와 props의 역할이 뒤섞이기 때문에, 대부분의 경우에는 필요하지 않습니다.

1-3 render()

  • JSX를 이용해서 UI를 리턴합니다.
  • 현재 state와 props를 바탕으로 화면에 무엇을 보여줄지 결정하는 단계입니다.

render는 언제 호출될까요?

  • 컴포넌트가 처음 마운트될 때
  • state가 변경될 때
  • props가 변경될 때
  • 부모 컴포넌트가 다시 렌더링될 때
  • render는 순수 함수처럼 동작해야 한다는 규칙이 있습니다.

즉,

  • 같은 props, 같은 state → 항상 같은 JSX 반환
  • 외부 상태를 바꾸지 않음
  • side effect를 만들지 않음

1-4 componentDidMount() (함수형 컴포넌트에서는 useEffect로 사용 가능)

  • 컴포넌트 마운트가 완료되고, 리액트가 JSX를 실제 DOM에 반영한 뒤 화면에 렌더링된 후 단 한 번 호출됩니다.
  • componentDidMount는 side effect를 수행하기 위한 대표적인 위치로 아래와 같은 작업을 수행합니다.
  • 서버로부터 데이터 요청 (API 호출)
  • DOM에 직접 접근해야 하는 초기 설정
  • 타이머 설정
  • 외부 라이브러리 초기화
  • 이벤트 리스너 등록
  • 함수형 컴포넌트의 경우 useEffect를 사용해 componentDidMount와 같은 역할을 수행하며, 의존성 배열을 빈 배열로 전달하면 최초 마운트 시 한 번만 실행됩니다.

2. 업데이팅(updating)

  • 마운트 된 컴포넌트의 상태(state)나 속성(props)이 변경되어 리렌더링 되는 단계

2-1 static getDerivedStateFromProps(props,state)

  • 1-2와 동일

2-2 shouldComponentUpdate(nextProps, nextState)

  • 컴포넌트가 다시 렌더링될지 말지를 결정하는 단계입니다.
  • state나 props가 변경되면 기본적으로 리액트는 다시 render를 호출하지만, 이 메서드를 사용하면 그 흐름을 중간에서 제어할 수 있습니다.
  • true를 리턴하면 이어서 render가 호출되고, false를 리턴하면 render를 호출하지 않습니다.
  • 생략할 경우 항상 true를 반환합니다.
  • 인자로 전달되는 nextProps, nextState와 이전 값 this.props, this.state를 비교해서 렌더링 여부를 결정할 수 있습니다.
  • Component 대신 PureComponenet를 상속 받을 경우 이 메서드가 이전과 현재의 props, state를 Object.is() 함수를 사용해 얕은 비교를 하여 바뀌지 않았다면 렌더링 하지 않도록 이미 구현되어 있습니다.
  • 함수형 컴포넌트에서는 shouldComponentUpdate를 직접 사용할 수 없습니다.

대신

  • React.memo
  • useMemo
  • useCallback

같은 도구로 같은 효과를 낸다.

게다가 잘못 사용하면 버그를 만들기 쉬워 보통은 PureComponent나 memo 기반 최적화를 사용하는 것이 권장됩니다.

2-3 render()

  • 1-3과 동일

2-4 getSnapshotBeforeUpdate(prevProps, prevState)

  • render() 메서드가 호출되어 가상 DOM으로 쓰기가 완료되고, 브라우저 DOM에 업데이트 되기 전에 호출됩니다.
  • 이 메서드의 티턴값이 2-5 componentDidUpdate()의 세 번째 인자로 전달됩니다.
  • DOM이 바뀌기 “직전 상태”를 기억해두는 역할을 합니다.

업데이트 전·후의 DOM 차이를 알아야 하는 경우를 위해 이 단계가 필요한데, 어떤 경우가 이에 해당할까요?

  • 스크롤 위치 유지
  • DOM 크기 변화 감지 등...
  • 주의할 점은 여기서는 side effect를 하면 안 된다는 겁니다.

2-5 componentDidUpdate(prevProps, prevState, snapshot) (함수형 컴포넌트에서는 useEffect로 사용 가능)

  • 브라우저 DOM 업데이트 완료 후에 호출됩니다.
  • 현재 속성 this.props, this.state와 이전 값 prevProps, prevState가 다르다면 외부 API 호출 등의 작업을 수행합니다.
  • 2-4에서 리턴한 값이 세 번째 인자 snapshot으로 전달되므로 보통 2-4와 같이 사용됩니다.
  • “화면이 실제로 바뀐 뒤” 실행되는 단계입니다.
  • 최초 마운트 시에는 호출되지 않고, props와 state가 변경되었을 때 호출됩니다.

이 단계는 업데이트 이후의 side effect 처리 공간으로 주로 아래와 같은 작업을 수행합니다.

  • props / state 변화에 따른 API 재요청
  • DOM 조작
  • 외부 라이브러리 업데이트
  • 주의할 점은 반드시 조건문으로 이전 값과 비교해야 무한 업데이트에 빠지지 않습니다.
  • 함수형 컴포넌트에서는 useEffect를 사용하여 이 단계를 수행합니다.

3. 언마운팅(unmounting)

  • 컴포넌트가 DOM에서 제거되는 단계입니다.

3-1 componentWillUnmount() (함수형 컴포넌트에서는 useEffect로 사용 가능)

  • 컴포넌트가 애플리케이션의 컴포넌트 트리에서 삭제되기 직전에 실행됩니다.
  • 주로 1-4 componentDidMount()와 세트로 사용됩니다.
    - 웹소켓을 사용할 경우 1-4에서 연결하고, 이곳에서 연결 해제합니다.
    • 1-4에서 setTimeout()을 호출했다면 이곳에서 clearTimeout()으로 해제됩니다.
  • 이 단계의 핵심 역할은 컴포넌트가 사용하던 외부 자원을 정리(clean up)하는 것입니다.

왜 정리가 필요할까요?

컴포넌트는 사라지지만,

  • 타이머
  • 이벤트 리스너
  • 웹소켓 연결
  • 외부 라이브러리

같은 것들은 자동으로 정리되지 않습니다.
정리를 하지 않으면

  • 메모리 누수
  • 의도치 않은 동작
  • 존재하지 않는 컴포넌트에 대한 상태 업데이트

같은 문제가 생길 수 있기 때문에 정리가 필요합니다.


4. 라이프사이클 메서드가 두 번씩 호출되는 이유

리액트에서 컴포넌트의 라이프사이클 메서드나 useEffect가 개발 환경에서 두 번 호출되는 현상은 버그가 아니라 의도된 동작입니다.
이는 Vite로 프로젝트를 생성했을 때 기본으로 적용되는 main.jsx의 때문이며, 개발 모드에서만 발생합니다.

Strict Mode란 무엇인가

<StrictMode>는 리액트 애플리케이션을 더 안전하게 만들기 위한 개발 도구로, 프로덕션 빌드에는 영향을 주지 않으며, 개발 중에만 잠재적인 문제를 미리 발견하도록 돕습니다.

Strict Mode에서는 컴포넌트가 다음과 같은 이유로 의도적으로 한 번 더 실행됩니다.

  • 컴포넌트가 순수 함수인지 확인하기 위해
  • Effect의 cleanup 누락을 확인하기 위해
  • 상태 관련 버그를 조기에 발견하기 위해
  • 더 이상 사용되지 않는 API에 대한 경고를 위해

리액트의 모든 컴포넌트는 순수 함수임을 가정하기 때문에 동일한 입력에 대해(props, state, context) 동일한 출력(JSX)을 반환해야 합니다.


이상 컴포넌트 생명주기(라이프 사이클)에 대한 정리를 마치겠습니다.

profile
웹 프론트엔드 개발자를 꿈꾸고 있습니다.

0개의 댓글