Javascript/React

컴포넌트 생명 주기와 useEffect, useLayoutEffet

kyoulho 2024. 7. 14. 19:41

리액트는 컴포넌트를 생성하고 렌더링 하다가 어떤 시점이 되면 소멸한다. 이러한 과정을 컴포넌트의 생명 주기라고 표현한다.

 

클래스 컴포넌트 예제

import {Component} from "react";
import {Title} from "../components";

export default class ClassLifecycle extends Component {
    state = {
        today: new Date(),
        intervalId: null as unknown as NodeJS.Timer
        // null 값을 unknown 타입으로 변환한 값을 다시 NodeJS.Timer 타입으로 변환한다.
        // 최종적으로 intervalId 변수는 null로 초기화되지만,
        // 타입스크립트에서는 이를 NodeJS.Timer 타입으로 인식하게 된다.
    }

    componentDidMount() {
        const duration = 1000
        const intervalId = setInterval(() => this.setState({today: new Date()}), duration)
        this.setState({intervalId})
    }

    componentWillUnmount() {
        clearInterval(this.state?.intervalId)
    }

    render() {
        const {today} = this.state
        return (
            <section className={"mt-4"}>
                <Title>ClassLifecycle</Title>
                <div className={"mt-4 flex flex-col items-center"}>
                    <p className={"font-mono text-3xml"}>{today.toLocaleTimeString()}</p>
                    <p className={"font-mono text-3xml"}>{today.toLocaleDateString()}</p>
                </div>
            </section>
        )
    }
}

상태 구현하기

클래스 컴포넌트에서 상태는 항상 state라는 이름의 멤버 속성으로 구현해야 한다는 제약 조건이 있다.

컴포넌트 마운트

리액트 컴포넌트는 가상 DOM객체 형태로 생성되어 어떤 시점에 물리 DOM 트리의 멤버 객체가 되며, 이 과정에서 처음 렌더링이 일어나는데 이 시점을 컴포넌트가 마운트 되었다고 표현한다. 리액트는 클래스 컴포넌트가 componentDidMount()라는 메서드를 가지면 마운트 되는 시점에 이 메서드를 호출해 준다.

setState() 메서드

클래스가 state라는 이름의 멤버 속성을 가지고 있다는 가정으로 설계된 메서드이다. 클래스 컴포넌트가 상태를 가지려면 반드시 state라는 멤버 속성이 있어야 하는 이유는 setState() 메서드가 이런 방식으로 구현되었기 때문이다.

 setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => Pick<S, K> | S | null) | (Pick<S, K> | S | null),
            callback?: () => void,
        ): void;
        
// 타입 정의가 복잡해 보이지만 대부분 다음처럼 호출한다.

const id = setInterval(콜백_함수, 시간_간격)
this.setState({intervalId: id})

컴포넌트 언마운트

컴포넌트가 생성되고 어떤 시점이 되면 소멸한다. 컴포넌트가 물리 DOM 객체로 있다가 소멸하는 것을 언마운트되었다고 표현한다.

리액트는 클래스 컴포넌트가 componentWillUnmount() 메서드를 구현하고 있으면 언마운트가 일어나기 직전에 이 메서드를 호출해 준다. setInterval() 함수는 더 이상 사용하지 않으면 반드시 clearInterval() 함수가 호출될 거라는 전제로 설계되었기에 반드시 호출하여 메모리 누수를 방지하자.

 

useLayoutEffect와 useEffect

두 훅은 사용법이 같으며 콜백 함수는 훅이 실행될 때 처음 한 번은 반드시 실행된다. 그리고 이런 설계는 클래스 컴포넌트의 componentDidMount() 메소드를 구현한 것과 사실상 같은 효과를 보인다. 두 훅이 componentDidMount()와 다른 점은 단 한 번 실행되는 componentDidMount() 와 달리, 의존성 목록 이 변경될 때마다 콜백 함수를 실행 한다는 점이다.

두 훅의 차이로는 useLayoutEffect 훅은 동기로 실행하고, useEffect 훅은 비동기로 실행된다. 리액트 공식 문서에서는 될 수 있으면 useEffect 훅을 사용하고 useEffect로 구현이 안될 때만 useLayoutEffect 훅을 사용하라고 권한다.

두 훅의 콜백 함수는 함수를 반환할 수도 있는데, 이때 반환 함수는 컴포넌트가 언마운트될 때 한 번만 호출된다.

useEffect(콜백_함수, 의존성_목록)
useLayoutEffect(콜백_함수, 의존성_목록)

콜백_함수 = () => {
    return 반환_함수
}

 

예제

DOM이나 window 객체에 이벤트 처리기를 부착하는 커스텀 훅이다. addEventListener() 메소드를 호출하면 반드시 removeEventListener() 메서드를 호출해 주어야 메모리 누수가 발생하지 않는다.

import {useEffect} from "react";

export const useEventListener = (
    target: EventTarget | null,
    type: string,
    callback: EventListenerOrEventListenerObject | null
) => {
    useEffect(() => {
        if (target && callback) {
            target.addEventListener(type, callback)
            return () => target.removeEventListener(type, callback)
        }
    }, [callback, target, type])

}