본문 바로가기

Front-end/React

[React] 리액트 컴포넌트 생명주기(Lifecycle) 총정리: 클래스형부터 useEffect 훅까지

리액트(React) 개발의 핵심 기초이자 면접 단골 질문인 컴포넌트 생명주기(Lifecycle API)에 대해 알아보겠습니다.
리액트 컴포넌트는 브라우저 화면에 나타나고, 데이터가 바뀌어 업데이트되고, 화면에서 사라지는 일련의 과정을 거칩니다. 이 흐름을 완벽히 이해해야 자원 낭비를 줄이는 코드 최적화와 올바른 데이터 연동(Side Effect) 처리가 가능합니다.
 
 

1. 리액트 생명주기 한눈에 보기 (개념도)

리액트 클래스형 컴포넌트의 전체적인 생명주기는 크게 Mount(생성), Update(수정), Unmount(소멸) 3가지 단계로 나뉩니다. 전체 흐름을 시각적으로 나타낸 다이어그램을 먼저 확인해 보세요.
 
🔄 핵심 흐름 요약
  • Mount (생성): ReactDOM.render() ➔ constructor() ➔ componentWillMount() ➔ render() ➔ componentDidMount()
  • Update (업데이트): 부모 변경/상태 변경 ➔ shouldComponentUpdate() ➔ componentWillUpdate() ➔ render() ➔ componentDidUpdate()
  • Unmount (제거): 컴포넌트 소멸 직전 ➔ componentWillUnmount()

 

2. 실무 코드로 보는 생명주기 메서드 (TypeScript)

부모 컴포넌트(App.tsx)에서 버튼을 눌러 리스트 데이터를 추가할 때, 부모와 자식 컴포넌트(ListComponentClass.tsx) 간에 생명주기 메서드가 어떤 순서로 찍히는지 코드로 구현해 보겠습니다.
 
 
 

📄 부모 컴포넌트: App.tsx

부모 컴포넌트는 list 상태를 가지고 있으며, 추가 버튼 클릭 시 새로운 항목을 배열에 더합니다
import React, { Component } from 'react';
import ListComponentClass from './ListComponentClass';

interface IListItem {
  id: string;
  name: string;
}

interface IState {
  list: IListItem[];
}

class App extends Component<any, IState> {
  constructor(props: any) {
    super(props);
    console.log("Parent: constructor (1. 초기 생성자)");
    this.state = {
      list: [
        { id: "1", name: "호랑이" },
        { id: "2", name: "사자" },
        { id: "3", name: "토끼" }
      ]
    };
  }

  // 데이터 추가 핸들러
  addRow = () => {
    let temp = Array.from(this.state.list);
    temp.push({
      id: (temp.length + 1).toString(),
      name: "호랑이"
    });
    this.setState({ list: temp }); // State 변경 -> Update 주기 시작!
  };

  componentWillMount() {
    console.log("Parent: componentWillMount (2. DOM 장착 전)");
  }

  componentDidMount() {
    console.log("Parent: componentDidMount (4. 첫 렌더링 완료 후)");
  }

  shouldComponentUpdate(nextProps: any, nextState: IState) {
    console.log("Parent: shouldComponentUpdate (업데이트 여부 판단)");
    return true; // 반드시 true를 리턴해야 render가 동작합니다.
  }

  componentWillUpdate(nextProps: any, nextState: IState) {
    console.log("Parent: componentWillUpdate (업데이트 직전)");
  }

  componentDidUpdate(prevProps: any, prevState: IState) {
    console.log("Parent: componentDidUpdate (부모 업데이트 완료!)");
  }

  render() {
    console.log("Parent: render (3. 렌더링 중)");
    return (
      <div style={{ padding: '20px', border: '2px solid blue' }}>
        <h2>부모 컴포넌트 (Parent)</h2>
        <button onClick={this.addRow} style={{ padding: '5px 10px', marginBottom: '10px' }}>
          🐯 호랑이 추가 버튼
        </button>
        {/* 자식 컴포넌트에 data로 변경된 list 전달 */}
        <ListComponentClass data={this.state.list} />
      </div>
    );
  }
}

export default App;

 

 

 

📄 자식 컴포넌트: ListComponentClass.tsx

부모로부터 data 배열을 props로 넘겨받아 목록을 출력해 주는 클래스 컴포넌트입니다
 
import React from 'react';

interface IProps {
  id: string;
  name: string;
}

class ListComponentClass extends React.Component<{ data: IProps[] }, any> {
  constructor(props: { data: IProps[] }) {
    super(props);
    console.log("Child: constructor (자식 생성)");
  }

  componentWillMount() {
    console.log("Child: componentWillMount");
  }

  componentDidMount() {
    console.log("Child: componentDidMount");
  }

  shouldComponentUpdate(nextProps: any, nextState: any) {
    console.log("Child: shouldComponentUpdate (자식 업데이트 여부 판단)");
    return true;
  }

  componentWillUpdate(nextProps: any, nextState: any) {
    console.log("Child: componentWillUpdate");
  }

  componentDidUpdate(prevProps: any, prevState: any) {
    console.log("Child: componentDidUpdate (자식 완료!)");
  }

  render() {
    console.log("Child: render (자식 렌더링 중)");
    return (
      <div style={{ marginTop: '10px', padding: '10px', border: '1px dashed red' }}>
        <h3>자식 컴포넌트 (Child)</h3>
        <ul>
          {this.props.data.map((item) => (
            <li key={item.id}>{item.id}번 동물: {item.name}</li>
          ))}
        </ul>
      </div>
    );
  }
}

 

 

 

3. 🔥 "추가" 버튼 클릭 시, 콘솔 로그 흐름 정밀 분석

 

부모 컴포넌트에서 버튼을 클릭하여 this.setState()가 수행되면 콘솔창에 찍히는 결과는 다음과 같습니다. 이 순서의 흐름을 면접에서 아주 자주 물어봅니다!

 

1. Parent: shouldComponentUpdate
2. Parent: componentWillUpdate
3. Parent: render
4. Child: shouldComponentUpdate
5. Child: componentWillUpdate
6. Child: render
7. Child: componentDidUpdate
8. Parent: componentDidUpdate

 

💡 왜 이 순서대로 찍힐까요? (중요 개념!)

  1. 부모의 상태 변경: 부모의 데이터가 바뀌었으므로 리액트는 부모에게 shouldComponentUpdate를 물어본 뒤, 변경 승인이 나면 가상 DOM을 다시 그리기 위해 Parent: render까지 순서대로 내려갑니다.
  2. 자식 컴포넌트로 전파: 부모가 리렌더링되면서 자식 컴포넌트에게 변경된 새로운 배열(data)을 토스합니다. 이에 따라 자식 컴포넌트 역시 자신의 업데이트 주기(shouldComponentUpdate ➔ componentWillUpdate ➔ render)를 연달아 시작합니다.
  3. 렌더링 완료는 역순(하향식 ➔ 상향식 완료): 리액트 구조상 하위에 속한 자식 노드들이 실제 브라우저 DOM에 완전히 그려져야(Child: componentDidUpdate), 비로소 부모 컴포넌트도 완료(Parent: componentDidUpdate)된 것으로 처리됩니다. 그래서 자식의 완료 로그가 부모보다 먼저 찍히게 됩니다.

 

4. 함수형 컴포넌트의 현대적인 처리: useEffect Hook

 

최신 리액트 개발에서는 클래스형 생명주기 메서드들의 복잡함을 덜기 위해, 함수형 컴포넌트에서 useEffect 단 하나의 훅(Hook)으로 통합하여 모든 Side Effect를 제어합니다.

 

 

import React, { useState, useEffect } from 'react';

function EffectExample() {
  const [count, setCount] = useState<number>(0);
  const [isInit, setInit] = useState<boolean>(false);
  const [isState, setIsState] = useState<any>(null);
  const [thisComponentStatus, setThisComponentStatus] = useState<any>(null);

  // 1️⃣ 의존성 배열이 없는 경우
  // 역할: componentDidMount + componentDidUpdate
  // 설명: 최초 마운트 때와 컴포넌트가 리렌더링 될 때마다 무조건 매번 실행됩니다.
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  // 2️⃣ 의존성 배열에 특정 상태가 기술된 경우
  // 역할: 조건부 componentDidUpdate
  // 설명: 최초 마운트 때 1번, 그리고 배열에 명시된 [isInit] 변수의 값이 "변경될 때만" 실행됩니다.
  useEffect(() => {
    if (isInit) {
      setCount(0);
      setInit(false);
    }
  }, [isInit]);

  // 3️⃣ 다중 조건 매핑
  // 설명: 배열 속의 값(isState 또는 thisComponentStatus) 중 하나라도 변경되면 트리거됩니다.
  useEffect(() => {
    console.log("관찰 중인 상태 중 하나가 변경되었습니다.");
  }, [isState, thisComponentStatus]);

  // 4️⃣ 빈 의존성 배열 [] + Clean-up 반환 함수
  // 역할: componentDidMount + componentWillUnmount
  // 설명: 첫 로드 시 브라우저 이벤트를 딱 한 번만 등록하고, 컴포넌트가 파괴(소멸)될 때 이벤트를 깔끔히 지워줍니다.
  useEffect(() => {
    const handleMouseMove = () => { /* 마우스 추적 이벤트 */ };
    window.addEventListener('mousemove', handleMouseMove);

    // [중요] return하는 익명 함수가 클래스형의 'componentWillUnmount' 역할을 수행합니다.
    return () => {
      window.removeEventListener('mousemove', handleMouseMove);
    };
  }, []); // 빈 배열([])을 주면 컴포넌트가 처음 켜질 때와 꺼질 때 딱 한 번씩만 호출됩니다.

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>클릭</button>
      <button onClick={() => setInit(true)}>초기화</button>
    </div>
  );
}

'Front-end > React' 카테고리의 다른 글

Zustand란? React 전역 상태 관리 쉽게 이해하기  (0) 2026.07.02
React Router, Rest 호출(Axios)  (0) 2026.07.02
[React] JSX, Props & State  (0) 2026.07.02
[React] 리액트 Basic  (0) 2026.07.02
[React] React란 React Overview  (1) 2026.07.02