본문 바로가기
프로그래밍/자바스크립트

[React] 생명주기(Lifecycle)과 Error Boundary

by pentode 2023. 6. 5.

React의 컴포넌트가 화면에 보여지고, 수정되고, 제거되는 생명주기(Lifecycle)될 때 내부적으로 다양한 함수들이 순차적으로 실행되어 처리됩니다. 이런 생명주기와 관계된 함수들을 알아봅니다.

참고로 테스트시에 각 함수에서 콘솔로 출력을 내는데 생명주기 함수가 두 번씩 호출됩니다. 이는 개발 환경에서 "index.tsx" 파일에서 <App /> 를 호출하는데 <React.StrictMode>를 사용하고 있기 때문이라고 합니다. build해서 나오는 배포판에는 이 부분이 빠지므로 문제가 없겠습니다.

 

root.render(
    <React.StrictMode>
        <App />
    </React.StrictMode>
);

 

생명주기 테스트용 소스

 

생명주기 테스트용 소스입니다. Hello.tsx파일로 App.tsx에서 부릅니다.

 

import { Component } from "react";

interface HelloProps { name: string }
interface HelloState { name: string, error:boolean }

class Hello extends Component<HelloProps, HelloState> {
    // 생성자
    constructor(props: HelloProps) {
        super(props);
        console.log("Hello - constructor()");

        this.state = { name: props.name, error: false };
    }

    // 정상적으로 이름을 변경합니다.
    onClickName = () => {
        this.setState((preState:HelloState, props:HelloProps) => {
            return {name:"홍길동"};
        });
    }

    // 상태 변경중 에러가 발생합니다.
    onClickStateError = () => {
        this.setState((preState:HelloState, props:HelloProps) => {
            throw new Error("상태 변경중 에러!");
        });
    }

    // 이벤트 처리기에서 에러가 발생합니다.
    onClickEventError = () => {
        try {
            throw new Error("이벤트 처리기에서 에러!");
        } catch(error) {
            this.setState({name:"", error:true});
        }
    }

    // 화면을 그립니다,
    render() {
        console.log("Hello - render()");

        if(this.state.error) {
            return <h1>이벤트 에러 발생!</h1>;
        }

        const { name } = this.state;

        return (
            <div>
                <h1>Hello {name}</h1>
                <button onClick={this.onClickName}>홍길동</button>
                <button onClick={this.onClickStateError}>상태 변경중 에러</button>
                <button onClick={this.onClickEventError}>이벤트 처리기 에러</button>
            </div>
        );
    }

    // 초기에는 render() 앞에, 수정시에는 shouldComponentUpdate 앞에 호출됩니다.
    static getDerivedStateFromProps(nextProps:HelloProps, prevState:HelloState) {
        console.log("Hello - getDerivedStateFromProps()");
        if(nextProps.name !== preState.name) {
            return { name: nextProps.name + ", " + prevState.name };
        }
        return null;
    }
    
    // 초기 로드시 render() 뒤에 호출됩니다.
    componentDidMount() {
        console.log("Hello - componenetDidMount()");
    }

    // props 또는 state가 새로운 값으로 업데이트 되면 render() 앞에 호출됩니다.
    shouldComponentUpdate(nextProps:HelloProps, nextState:HelloState) {
        console.log("Hello - shouldComponentUpdate(nextProps, nextState)");
        return true;
    }

    // 업데이트시 render() 뒤에, 렌더링 결과가 DOM에 반영되기 전에 호출됩니다.
    getSnapshotBeforeUpdate(prevProps:HelloProps, prevState:HelloState) {
        console.log("Hello - getSnapshotBeforeUpdate(prevProps, prevState)");
        //return null;
        return prevState.name;
    }

    // 업데이트시 getSnapshotBeforeUpdate()뒤에, 렌더링 결과가 DOM에 반영된 후 호출됩니다.
    componentDidUpdate(prevProps:HelloProps, prevState:HelloState, snapShot:string) {
        console.log("Hello - componentDidUpdate(prevProps, prevState, snapShot)");
        console.log("snapShot: " + snapShot);
        console.log("name : " + this.state.name);
    }

    // 컴포넌트가 제거되기 직전에 호출됩니다.
    componentWillUnmount() {
        console.log("Hello - componentWillUnmount()");
    }
}

export default Hello;

 

최초 생성시 실행순서

 

처음 컴포넌트가 로드되어 보여질때의 생명주기 함수의 실행 순서 입니다.

 

1. Hello - constructor()
2. Hello - getDerivedStateFromProps()
3. Hello - render()
4. Hello - componenetDidMount()

 

1. 생성자 constructor() 함수

컴포넌트 객체의 생성자입니다. 먼저 super(props)를 호출하고, props는 상위 컴포넌트로부터 받은 값입니다.

2. static getDerivedStateFromProps(nextProps, preState) 함수

상위컴포넌트로 받은 props값과 state 값을 인자로 받습니다. 반환값은 새로운 상태를 반환하면 그 상태로 상태값이 바로 변경이 됩니다. null을 반환하면 상태의 변화가 없습니다. static 함수이므로 내부에서 this를 사용할 수 없습니다. 잘 사용되지 않는 함수라고 합니다.

3. render() 함수

받은 속성및 상태들을 이용해서 화면에 보여질 내용을 만들어 반환합니다.

4. componentDidMound() 함수

컴포넌트가 render()함수가 반환한 값으로 DOM에 삽입된 후 호출됩니다. 이런 초기화 과정을 마운트라고 합니다.

 

컴포넌트 업데이트시 실행순서

 

컴포넌트의 내용이 업데이트(상태가 수정)되면 실행되는 순서 입니다.

 

1. Hello - getDerivedStateFromProps()
2. Hello - shouldComponentUpdate(nextProps, nextState)
3. Hello - render()
4. Hello - getSnapshotBeforeUpdate(prevProps, prevState)
5. Hello - componentDidUpdate(prevProps, prevState, snapShot)

 

1. getDerivedStateFromProps() 함수

로드시와 동일한 함수 입니다.

2. shouldComponentUpdate(nextProps, prevState) 함수

성능최적화를 위해서 사용된다고 합니다. 인자로 속성과 상태를 받습니다. 반환값은 boolean타입입니다. true를 반환하면 다음단계(render())를 실행해서 업데이트 하고, false를 반환하면 업데이트를 중지합니다.

3. render() 함수

로드시와 동일한 함수 입니다.

4. getSnapshotBeforeUpdate(prevProps, prevState) 함수

render()가 반환한 정보로 DOM을 업데이트하기 전에 실행됩니다. 업데이트 전의 값들을 가져와서 반환하면 DOM이 업데이트된 후에 실행될 componentDidUpdate()함수의 세 번째 인자로 들어가게됩니다. 변경전 정보를 저장해서 처리하므로 스냅샷이라는 이름이 붙었습니다.

5. componentDidUpdate(prevProps, prevState, snapShot) 함수

업데이트가 완료되면 실행됩니다.

 

컴포넌트가 제거될 때 실행순서

 

컴포넌트가 제거될 때 실행되는 함수입니다. 하나의 함수가 실행됩니다.

 

1. Hello - componentWillUnmount()

 

1. componentWillUnmount() 함수

컴포넌트가 마운트 해제되어 제거되기 직전에 호출됩니다.

 

Error Boundary 테스트 소스

 

App.tsx파일입니다. Hello.tsx를 사용하고, 에러 처리를 위해서 ErrorBoundary객체를 사용합니다.

 

import React, { Component, ReactNode, ErrorInfo } from "react";
import Hello from "./Hello";

interface AppProps {}

class App extends Component<AppProps> {

    // 생성자
    constructor(props: AppProps) {
        super(props);
        console.log("App - constructor()");
    }

    // 렌더링 함수
    render() {
        console.log("App - render()");
        return (
            <ErrorBoundary>
                <Hello name="React" />
            </ErrorBoundary>
        );
    }
}


interface Props { children?: ReactNode }
interface State  { hasError: boolean }

class ErrorBoundary extends Component<Props, State> {

    public state: State = { hasError: false };

    // 에러정보를 state에 저장해 화면에 나타내는 용도
    static getDerivedStateFromError(error:Error): State {
        console.log("ErrorBoundary - getDerivedStateFromError(error:Error)");
        return { hasError: true };
    }

    // 에러 정보를 서버로 전송하는 용도
    componentDidCatch(error: Error, errorInfo:ErrorInfo) {
        console.log("ErrorBoundary - componentDidCatch()");
        //this.setState({hasError: true});
    }

    public render() {
        if(this.state.hasError) {
            console.log("ErrorBoundary - render()");
            return <h1>에러가 발생했습니다.</h1>;
        }

        return this.props.children;
    }
}

export default App;

 

Error Boundary를 이용한 에러처리

 

컴포넌트에서 발생한 에러를 직접 try/catch등을 사용해서 처리할 수도 있지만, 에러 경계를 사용해서 처리할 수도 있습니다. 컴포넌트의 에러는 상위로 올라가다가 처음 만나는 에러 경계에서 처리가 됩니다. 에러 경계 컴포넌트가 에러를 잡으면 자체 내용을 보여주게 됩니다.

 

<ErrorBoundary>
    <Hello name="React" />
</ErrorBoundary>

 

컴포넌트가 getDerivedStateFromError()함수와 componentDidCatch()함수 중 하나를 가지거나 둘 모두를 가지면 에러 경계 객체가 됩니다.

주의할 것은 에러 경계로 잡을 수 없는 에러가 있습니다.

- 이벤트 핸들러에서 발생한 에러

- 비동기적 코드에서 발생한 에러(setTimeout 등)

- 서버 사이드 렌더링에서 발생한 에러

- 에러 경계 자체에서 발생한 에러

 

에러 발생시 실행순

 

위 예제에서 Hello 컴포넌트에서 에러가 발생한 경우 입니다. 테스트시 getDerivedStateFromError()함수와 render()함수가 두번씩 호출되었는데, 원인을 찾지 못했습니다. 아마도 react의 버그일수도 있을것 같습니다.

 

1. ErrorBoundary - getDerivedStateFromError(error:Error)
2. ErrorBoundary - render()
3. Hello - componentWillUnmount()
4. ErrorBoundary - componentDidCatch()

 

1. getDerivedStateFromError(error:Error) 함수

에러가 발생했음을 반환하여 render() 함수에서 처리할 수 있도록 합니다. 에러 상태정보를 저장해서 화면에 보여주는 용도로 사용합니다.

2. render() 함수

필요에 따라 자체 에러 메세지를 내거나 하위 화면을 그대로 보여주거나 할 수 있습니다.

3. Hello컴포넌트의 componentWillUnmount() 함수

에러가 발생한 Hello 컴포넌트가 마운트 해제되면서 호출된 함수 입니다.

4. componentDidCatch() 함수

에러 정보를 서버로 전송하는 용도로 사용합니다.

반응형