React 컴포넌트 생명주기 뿌수깅

leetrueleetrue
10 min read

♻️ 돌고 돌아 React 생명주기

모든 리액트 컴포넌트는 생명주기가 존재한다. React 의 생명주기는 다빈도 면접 질문이기도 한데, React 개발자가 생명주기를 알아야하는 이유는생명주기에 따라서 어떤 작업을 처리해줘야 하는지 지정해줘야 불필요한 리렌더링을 방지할 수 있기 때문이다.

실제로 개발을 하다보면, 이 생명주기를 고려해서 작업을 해줘야하는 일이 많이 생긴다. 생명주기를 이해하지 못한채로 개발을 하다보면, 결국 벽에 부딪히고 만다. 그래서 생명주기에 대해서 잘 정리해두고 개념을 잘 이해해두면, 많은 도움이 되니 나도 머릿속에만 있던 것들을 한번 정리하는 시간을 가져보려 한다.


👶🏻 React 생명주기

생명주기라고 하면 말 그대로 컴포넌트가 태어나고, 변하고, 죽는(?).. 소멸하는 순간까지의 무언가를 의미하는데, 간단히 컴포넌트가 생성(Mounting) → 업데이트(Updating) → 제거(Unmounting) 로 이루어진 생명주기를 가졌다고 정리할 수 있겠다.

📗 React 생명주기에서 다루는 용어들

용어설명
~ will어떤 작업 수행 전 실행되는 메서드 관련 용어
~ did어떤 작업 수행 후 실행되는 메서드 관련 용어
mount컴포넌트에서 DOM이 생성되고 웹 브라우저 상에 나타나는 메서드 관련 용어
unmount컴포넌트 내에서 DOM을 제거하는 메서드 관련 용어
update컴포넌트 내에서 변화 발생 시 수행되는 것을 의미

👧🏻 Class Component Lifecycle 알아보기

사실 리액트 훅이 나오고.. 클래스형 컴포넌트를 쓸 일이 거의 없긴 하지만, 전반적인 큰 그림을 여기서 이해를 잡고 가면 좋다. 꽤 큰 도움이 되는듯! 그래서 한번 쭉 훑어봤다.

♻️ 생명주기 순서

뭔가 리액트 처음 배울 때 부터 리액트 생명주기와 관련한 그림은 많이 봐왔지만 나에게 그리 와닿지 못했던 것 같아서 직접 그려봤다. (라고 하기엔 그냥 이모지만 추가되긴 했지만, 의미에 맞는 이모지를 넣어보며 이해하니 훨씬 와닿았다.) 아래 다이어그램이 내가 이해한 리액트 컴포넌트 생명주기이다.

위 다이어그램을 보면 뭔가 크게 세가지로 나뉘어진 것이 두 종류가 보인다.

  • Render Phase → Pre-commit Phase → Commit Phase 🌲

  • Mounting → Updating → Unmounting

우선 이 크게 나뉘어진 갈래들에 대해서 알아보자.

🚨 알아보기 전에 미리 짚어보자, 리액트에서 말하는 렌더링이란? 렌더링은 모든 컴포넌트에게 현재 state와 props값의 조합을 기반으로 각각의 UI를 어떻게 화면에 띄우고 싶어 하는지 물어보는 React의 프로세스입니다. - Mark Erikson (Redux Maintainer)

렌더링은 각각의 컴포넌트마다, 자기 자신을 어떻게 표시하고 싶은지, 렌더링에 해당되는 모든 컴포넌트를 돌며 확인하는 과정이다. ReactElement 객체는 type(태그명), props(attribute), children 등에 대한 정보를 포함한다.

이 부분에 대해서는 아래 포스팅에서 더 자세히 볼 수 있다.

📆 Render / Commit Phase

1️⃣ Render Phase

🧮 컴포넌트를 렌더링 하고, 필요한 변화를 계산하는 작업

렌더링이 트리거가 되었을 때, 리액트는 ReactFiberWorkLoop 에서 렌더가 필요한 Fiber 부터 순차적으로 beginWork() 함수가 실행되며, 새로운 virtual DOM Fiber 를 만드는 작업을 완료하면서 completeWork() 함수를 호출한다. 이 때, 처음 dirty 가 생긴 Fiber 에서의 completeWork() 호출이 끝나면 commit phase 로 넘어가게 된다. 이 과정에 대해서는 이번에 공부하면서 좀 열심히 파봤는데, 워낙 내용이 길어서 나중에 따로 다뤄보려한다! 넘모 재밌음!!

🤓 하지만 말이 어려웠으니 다시 쉽게 풀자면, 새로운 Virtual DOM 생성 후 이전 Virtual DOM 과 비교해 달라진 부분 확인하는 단계이다. 그리고 이 Render Phase 를 진행하는 동안 diffing 알고리즘을 이용해서 변경점들에 대해 effect list 를 생성해두는데, 이 리스트는 commit phase 에서 실행되게 된다.

2️⃣ Commit Phase

📝 변경 사항을 실제 DOM 에 적용하는 단계

effect list 에 있는 변경 사항들에 대해 real DOM 에 반영하며 모두 진행한다.

📝 정리

그러니 정리하자면 렌더링 과정은 크게 두 단계인데, 1️⃣ VDOM 을 생성해서 이전 VDOM 과 비교하는 과정을 가지며 달라진 부분들을 수집하는 단계(Render Phase)와 2️⃣ 이렇게 수집한 것들을 실제 DOM 에 반영하는 단계(Commit Phase)로 이루어진다는 것이다.


🌐 Mounting → Updating → Unmounting

1️⃣ Mounting

Mount 는 컴포넌트의 인스턴스가 생성되어서 DOM 상에 삽입되는 것으로 리액트에서는 컴포넌트를 특정 영역에 끼워넣는 행위를 의미한다. 예시를 들자면, ReactDOM.render 함수를 통해서 DOM 의 특정 영역에 리액트 컴포넌트를 끼워 넣을 수 있고, 이러한 과정을 마운트한다고 표현한다.

앞서 설명했던 렌더링 개념과 함께 생각하면 좋을 것 같은데, 렌더링은 컴포넌트가 DOM을 만드는 명령들을 반환하는 함수가 호출( 📢)되는 것을 의미하고, 마운팅은 컴포넌트를 처음으로 렌더링( 📢)하는 것을 의미한다.

📝 mount 단계에서의 Class component 생명주기 메서드 호출 순서는 아래와 같다.

👶🏻 constructor → 🎁 getDerivedStateFromProps 👗 → 📢 render 🌲 → ♻️ React updates DOM and refs → 🪄 componentDidMount

Mounting - 👶🏻 constructor()

컴포넌트가 새로 생성되고 렌더링(DOM 생성) 이전에 수행되는 클래스 생성자 메서드

constructor(props) {
    super(props);
    this.state = { counter: 0};
    this.handleClick = this.handleClick.bind(this);
}

생성한 함수를 바인딩 하고, state 공간에 변수값을 초기화할 때 사용한다. this.state에 객체를 할당하여 지역 state를 초기화하며, 인스턴스에 이벤트 처리 메서드를 바인딩한다.

컴포넌트에서 작성할 때는 아래와 같이 작성하면 된다.

import React from 'react';

class LifecycleClassComponent extends React.Component<any, any> {
    constructor(props: any) {
        super(props);
        this.state = {
            userId: "dobby",
            userAge: 100
        };
    };

    render() : React.ReactNode {
        return (
            <div>
                <h1>Main ClassComponent</h1>
                <div>{this.state.userAge}</div>
            </div>
        );
    };
};

export default LifecycleClassComponent;

Mounting - 📢 render() 🌲

미리 구현한 HTML 을 화면상에 보여주는 메서드

import React from 'react';

class LifecycleClassComponent extends React.Component<any, any> {
    render() : React.ReactNode {
        return (
            <div>
                <h1>Main ClassComponent</h1>
                <div>{this.state.userAge}</div>
            </div>
        );
    };
};

export default LifecycleClassComponent;

해당 메서드 안에서 부모 컴포넌트로 전달받은 props 값에 대해 접근이 가능하며, constructor() 에서 정의한 state 값의 접근도 가능하다. 하지만 이 공간에서는 setState() 는 사용할 수 없다.

Mounting - 🪄 componentDidMount()

컴포넌트 내에서 렌더링이 수행된 이후에 실행되는 메서드

import React from 'react';

class LifecycleClassComponent extends React.Component<any, any> {
    /* 컴포넌트가 새로 생성이 될 때 마다 호출되는 클래스 생성자 메서드를 의미 */
    constructor(props: any) {
        super(props);
        this.state = {
            userId: "dobby",
            userAge: 100
        };
    };

    /* 컴포넌트 내에서 렌더링이 수행된 이후에 실행 되는 메서드 */
    componentDidMount = (): void => {
        console.log("화면이 렌더링 된 이후 수행: componentDidMount()");
    };

    /* 미리 구현한 HTML을 화면상에 보여주는 메서드 */
    render() : React.ReactNode {
        return (
            <div>
                <h1>Main ClassComponent</h1>
                <div>{this.state.userAge}</div>
            </div>
        );
    };
};

export default LifecycleClassComponent;

2️⃣ Updating

Update 는 이미 mount 되어 DOM에 존재하는 컴포넌트를 re-rendering 하여 업데이트 하는 것으로, 부모로부터 전달받은 props 값이 변화했거나 부모 컴포넌트가 리렌더링하는 경우, 또는 해당 컴포넌트 내에서 state 값이 변한 경우 수행된다.

📝 update 단계에서의 Class component 생명주기 메서드 호출 순서는 아래와 같다.

🆕 New props / setState() →
🚦shouldComponentUpdate → if (update ⭕ ) → 📢 render() 🌲 → 📸 getSnapshotBeforeUpdate 🎑
→ ♻️ React updates DOM and refs → 🪄 componentDidUpdate 🎑

Updating - 🪄 componentDidUpdate 🎑

컴포넌트 내에서 변화가 발생했을 경우에 실행되는 메서드

import React from 'react';

class LifecycleClassComponent extends React.Component<any, any> {
    /* 컴포넌트가 새로 생성이 될 때 마다 호출되는 클래스 생성자 메서드를 의미 */
    constructor(props: any) {
        super(props);
        this.state = {
            userId: "dobby",
            userAge: 100
        };
    };

    /**
    * 컴포넌트 내에서 변화가 발생했을 경우 실행되는 메서드
    * @param prevProps: 이전 Props 값
    * @param prevState: 이전 State 값
    */
    componentDidUpdate = (prevProps: any, prevState: LifycycleType): void => {
        if (this.props.appState !== prevProps.appState) {
            console.log("전달 받은 props의 값에 변화가 생겼을 경우 수행");
        };
        // State 값 중 userAge의 값 변화가 생겼을 경우 수행됨
        if (this.state.userAge !== prevState.userAge) {
            console.log("사용자 나이의 변화가 발생했을 경우 수행");
        };
    }
};

export default LifecycleClassComponent;

3️⃣ Unmounting

마운트의 반대 과정으로 컴포넌트가 DOM에서 제거되는 것을 의미한다. unmount 단계의 class component에서는 componentWillUnmount() method가 호출된다.

🪄 componentWillUnmount ⚰️

Unmounting - 🪄 componentWillUnmount ⚰️

컴포넌트 내에서 DOM 을 제거할 때 실행되는 메서드

컴포넌트가 마운트 해제되어 제거되기 직전에 호출된다. 이 메서드 내에서 주로 DOM에 직접 등록했었던 이벤트를 제거하고, 만약에 setTimeout 을 걸은 것이 있을 때에는 clearTimeout 을 통해 제거할 수 있다.

이 때, 컴포넌트는 다시 렌더링되지 않기 때문에 componentWillUnmount() 내에서 setState() 를 호출할 수는 없다. 컴포넌트 인스턴스가 마운트 해제되고 나면 절대로 다시 마운트되지 않는다.

import React from 'react';

class LifecycleClassComponent extends React.Component<any, any> {
    constructor(props: any) {
        super(props: any);
        this.state = {
            userId: "dobby",
            userAge: 100,
            isShowTempComponent: true
        };
    };

    deleteTempComponent = (): void => {
        this.setState({
            ...this.state,
            isShowTempComponent: false,
        });
    };

    render(): ReactReactNode {
        return (
            <div>
                <h1>Main ClassComponent</h1>
                {
                    this.state.isShowTempComponent && (
                        <LifeCycleTempComponent />
                    )
                }
                <button onClick={this.deleteTempComponent>컴포넌트 제거</button>
            </div>
        )
    }
};

import React, { Component } from 'react';
class LifeCycleTempComponent extends Component<any, any> {
    constructor
}

🪝 함수형 컴포넌트 생명주기

함수형 컴포넌트는 React Hook 이라고도 하며 React 16.8 버전 이후부터 React 요소로 추가되었다.

위 그림만 봐도, 뭔가 클래스형 컴포넌트와 비슷한듯 보이면서도 조금 다르게 보인다. use뭐시껭이 들이 많이 보이는데 이것들을 리액트 훅이라고 부른다. 훅에 대해서는 간단하게 아래에서 다시 살펴보려 하는데, 간단히 설명하자면 함수 컴포넌트에서 리액트의 state 와 라이프 사이클을 연동시켜주는 함수를 의미한다.

함수형 컴포넌트에서는 클래스형 컴포넌트의 라이프사이클 메서드들에 대해서 아래와 같이 사용하고 있다. 아래 표를 보면 useEffect 가 mount, update, unmount 모두에서 사용되는 것을 확인할 수 있다. 그렇듯 함수 컴포넌트에서 useEffect 는 생명주기를 이용하는 것의 핵심이라 생각할 수 있겠다.

분류클래스형 컴포넌트함수형 컴포넌트
Mountingconstructor()함수형 컴포넌트 내부
Mountingrender()return()
MountingcomponentDidMount()useEffect()
UpdatingcomponentDidUpdate()useEffect()
UnMountingcomponentWillUnmount()useEffect()

🪝 Hook 에 대해서.

리액트 훅은 함수 컴포넌트에서 react state 와 라이프 사이클 기능을 연동할 수 있게 해주는 함수이다. hook은 Class 없이 React를 사용할 수 있게 해준다. so good..

👩🏻‍🏫 React Hook 도입 배경

기존의 컴포넌트 사이에서 상태 로직을 재사용하기 어려운 단점과 복잡한 컴포넌트들 같은 경우에는 이해가 어려운 문제가 있었다. 이러한 문제들 때문에 hook 이 도입이 되었고, 이를 통해서 Class 없이도 React 기능들을 사용할 수 있게 되었다. 결론적으로는 클래스형 컴포넌트들에 대한 단점을 극복하기 위해 함수 컴포넌트와 훅이 도입이 된 것이다.

♻️ 함수 컴포넌트 라이프 사이클

  1. 함수형 컴포넌트가 호출

  2. 함수형 컴포넌트 내부에서 실행

  3. return() 으로 화면에 렌더링

  4. useEffect() 실행

👀 함수 컴포넌트 생명주기 살펴보기

1️⃣ Mounting

Mounting - 📢 function() {} 🧱 내부

컴포넌트가 호출 시 가장 먼저 호출되는 공간으로, state 나 함수들을 정의하는 공간

const LiftcycleFunctionComponent: React.FC = (props: any) => {
    /**
    * 컴포넌트 호출 시 가장 먼저 호출 되는 공간
    * 컴포넌트에서 사용 될 state나 함수들을 정의하는 공간
    */

    const [userInfo, setUserInfo] = useState<any>({
        userId: 'poordobby',
        userAge: 100,
        isShowTempComponent: false
    });

    return (
        <div>
            <h1>야야야</h1>
        </div>
    )
};

export default LiftcycleFunctionComponent;

Mounting - 👶🏻 return ()

미리 구현한 HTML를 화면상에 보여주는 메서드

const LiftcycleFunctionComponent: React.FC = (props: any) => {
    const [userInfo, setUserInfo] = useState<any>({
        userId: 'poordobby',
        userAge: 100,
        isShowTempComponent: false
    });

    /**
    * 미리 구현한 HTML을 화면상에 보여주는 메서드
    */
    return (
        { console.log("rendering!") }
        <div>
            <h1>야야야</h1>
        </div>
    )
};

export default LiftcycleFunctionComponent;

2️⃣ ♻️ useEffect 🧹

해당 메서드로 Mounting/Updating/Unmounting 처리가 가능

useEffect(function, [deps]);
deps의 값구조설명
값이 없을 경우useEffect(() ⇒ {})화면 렌더링 이후 수행되며 리렌더링 발생하는 경우 다시 수행됨
빈 배열의 경우useEffect(() ⇒ {}, [])화면 렌더링 이후에만 수행됨
배열 값이 존재하는 경우useEffect(() ⇒ {}, [deps])화면 렌더링 이후 수행되고, 의존성 배열이 변경되었을 경우 해당 메서드가 수행됨

♻️ useEffect 🧹 - 👶🏻 mounting

: 컴포넌트 내에서 렌더링이 수행된 이후에 실행이 되는 메서드

const LiftcycleFunctionComponent: React.FC = (props: any) => {
    const [userInfo, setUserInfo] = useState<any>({
        userId: 'poordobby',
        userAge: 100,
        isShowTempComponent: false
    });

    /**
    * 컴포넌트 내에서 렌더링이 수행된 이후에 실행되는 메서드
    * @returns { void }
    */

    useEffect(() => {
        console.log("화면이 렌더링 된 이후에 바로 수행");
    }, []);

    return (
        { console.log("rendering!") }
        <div>
            <h1>야야야</h1>
        </div>
    )
};

export default LiftcycleFunctionComponent;

♻️ useEffect 🧹 - 🆕 updating

컴포넌트 내에서 변화가 발생하였을 경우에 실행되는 메서드

useEffect(callBackFunc, []);

위와 같이 의존성 배열이 비어있다면, 컴포넌트가 최초 렌더링 되었을 때만 실행된다. 이 때는 componentDidMount 역할을 수행한다.

useEffect(callBackFunc, [state1, state2]);

만약 의존성 배열에 위와 같이 변하는 값을 넣는다면, 해당 값들이 변화했을 때 callbackFunc 이 실행된다. 이 때는 🪄componentDidUpdate, 🎁 getDerivedStateFromProps의 역할을 수행한다.

사용 예시는 아래 코드를 참고해보자.

const LiftcycleFunctionComponent: React.FC = (props: any) => {
    const [userInfo, setUserInfo] = useState<any>({
        userId: 'poordobby',
        userAge: 100,
        isShowTempComponent: false
    });

    /**
    * 컴포넌트 내에서 변화가 발생한 경우 실행되는 메서드
    */

    useEffect(() => {
        console.log("props 변화 / state 변화 : componentDidUpdate()와 동일");
    }, [props.appState, userInfo.userAge]);

    return (
        { console.log("rendering!") }
        <div>
            <h1>야야야</h1>
        </div>
    )
};

export default LiftcycleFunctionComponent;

♻️ useEffect 🧹 - ⚰️ Unmounting

컴포넌트 내에서 DOM을 제거할 때에 실행이 되는 메서드

useEffect는 clean-up 함수를 return 할 수 있는데 clean-up 함수를 활용해서 컴포넌트가 unmount 될 때 정리해야할 것들을 처리할 수 있다.

const LifeCycleUnmountComponent = () => {
    useEffect(() => {
        return () => {
            console.log("잘가...");
        };
    }, []);

    return ( <h1>Unmount</h1> );
}

const LiftcycleFunctionComponent: React.FC = (props: any) => {
    const [isShow, setIsShow] = useState<boolean>(false);
    const [userInfo, setUserInfo] = useState<LifeCycleType>({
        userId: 'poordobby',
        userAge: 100,
        isShowTempComponent: false
    });

    const fn_unmountComponent = () => {
        setIsShow(!isShow);
    };

    return (
        { console.log("rendering!") }
        <div>
            <h1>야야야</h1>
            <button onClick={fn_unmountComponent}>컴포넌트 제거</button>
            { isShow && (<LifeCycleUnmountComponent/>) }
        </div>
    )
};

export default LiftcycleFunctionComponent;

React 생명주기가 이해가 갔더라도, 또 내가 작성한 것들이 어떤 순서로 실행되고 있는지 헷갈릴 때가 많다. 이 부분에 대해서 어느정도 이해를 하기위해 공부했던 것이 있는데, 필요하다면 아래 포스팅을 참고해봐도 좋을 것 같다. 실행 컨텍스트를 공부하면서 컴포넌트 내에서의 코드들의 실행 순서에 대해 알아본 포스팅이다.

[React] 실행 컨텍스트 in React


👩🏻‍🌾 정리

리액트 생명주기는 매번 공부를 해도 계속 망각되기도 하고 헷갈릴 때가 많다. 그래도 이번 기회에 다시 한 번 정리하면서, 렌더링에 대해서도 돌아볼 수 있어서 좋았던 것 같다. 여기서 조금 더 심화 과정으로 ReactFiberWorkLoop 도 한 번 후벼파봤는데, 나중에 시간 괜찮을 때 글로 정리를 해봐야겠다. 재밌었따. 킹아!


📦 참고

1
Subscribe to my newsletter

Read articles from leetrue directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

leetrue
leetrue

직면하는 모든 문제에 유치한 것은 없으며, 의미 없는 삽질 또한 없다고 믿습니다.