효과 처리와 비동기 작업 (useEffect), 컴포넌트의 라이프사이클과 useEffect의 활용

리액트(React)는 UI 구성을 위한 가장 인기 있는 라이브러리 중 하나로서, 현대 웹 애플리케이션의 프론트엔드 개발에 광범위하게 사용되고 있습니다. 리액트의 핵심 개념 중 하나는 컴포넌트입니다. 컴포넌트는 독립적으로 관리되는 UI의 일부분으로, 각각의 컴포넌트는 상태(state)와 생명주기(lifecycle)를 가집니다. 이 글에서는 리액트의 useEffect 훅에 대해 자세히 설명하고, 컴포넌트의 생명주기와 어떻게 비동기 작업을 관리할 수 있는지에 대해 살펴보겠습니다.

1. 컴포넌트의 생명주기(lifecycle)

리액트 컴포넌트의 생명주기는 크게 세 가지 단계로 나눌 수 있습니다: 마운트(mount), 업데이트(update), 언마운트(unmount)입니다. 각 단계에서는 컴포넌트가 상태 변화나 부모 컴포넌트의 변화에 따라 어떻게 행동하는지를 정의할 수 있습니다.

  • 마운트(Mount): 컴포넌트가 DOM에 처음 렌더링될 때 발생합니다. 이 단계에서 주로 초기 상태를 설정하거나 API를 호출하는 등의 작업을 수행합니다.
  • 업데이트(Update): 컴포넌트의 상태나 props가 변화할 때 발생합니다. 상태 변화에 따라 UI를 재렌더링하거나 관련 작업을 수행할 수 있습니다.
  • 언마운트(Unmount): 컴포넌트가 DOM에서 제거될 때 발생합니다. 이 단계에서 클린업(cleanup) 작업을 수행할 수 있습니다.

2. useEffect 훅의 역할

useEffect 훅은 컴포넌트의 생명주기 메서드를 대체하는 데 사용됩니다. 이 훅은 컴포넌트가 마운트되거나 업데이트 될 때 특정 작업을 수행하도록 정의할 수 있습니다. useEffect는 다음과 같은 용도로 사용됩니다:

  • 데이터 fetching (API 호출)
  • 구독(subscription)
  • DOM 업데이트
  • 타이머 설정 및 클린업

3. useEffect 사용 방법

useEffect 훅은 다음과 같이 사용됩니다:

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

const MyComponent = () => {
    const [data, setData] = useState(null);

    useEffect(() => {
        // API 호출
        fetch('https://api.example.com/data')
            .then(response => response.json())
            .then(json => setData(json));

        // 클린업 함수 리턴
        return () => {
            // 필요시 클린업 작업 수행
        };
    }, []); // 의존성 배열

    return (
        
{data ?

{data.title}

:

Loading...

}
); };

위의 예제에서 useEffect는 컴포넌트가 처음 마운트될 때 API를 호출하여 데이터를 가져오는 역할을 합니다. 의존성 배열이 빈 배열([])로 설정되어 있기 때문에, 이 effect는 컴포넌트가 최초로 마운트될 때 단 한 번만 실행됩니다. 데이터 fetching이 완료되면 setData를 호출하여 상태를 업데이트하고, 컴포넌트가 다시 렌더링됩니다.

4. 비동기 작업과 useEffect

리액트에서는 비동기 작업을 할 때 주의해야 할 점이 있습니다. 예를 들어, 컴포넌트가 언마운트된 후에도 비동기 작업이 완료되면 상태를 업데이트하려고 할 경우 오류가 발생할 수 있습니다. 이를 방지하려면 효과의 클린업 기능을 활용해야 합니다.

클린업 함수는 useEffect가 반환하는 함수로, 컴포넌트가 언마운트되거나 의존성 배열의 값이 변경될 때 호출됩니다.

5. 예제: 비동기 데이터 fetching과 오류 처리

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

const DataFetchingComponent = () => {
    const [data, setData] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);

    useEffect(() => {
        const fetchData = async () => {
            try {
                const response = await fetch('https://api.example.com/data');
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                const jsonData = await response.json();
                setData(jsonData);
            } catch (error) {
                setError(error);
            } finally {
                setLoading(false);
            }
        };

        fetchData();

        return () => {
            // 클린업 함수
            setData(null);
            setError(null);
        };
    }, []);

    if (loading) return 

Loading...

; if (error) return

{error.message}

; return (

Data:

{JSON.stringify(data, null, 2)}

);
};

위의 예제에서는 fetchData라는 비동기 함수를 정의하고, 이를 useEffect 내에서 호출합니다. 에러 처리를 위해 setError를 사용하여 API 호출 중 발생할 수 있는 네트워크 오류를 포착합니다. 로딩 상태는 loading 변수를 통해 관리하고 있습니다. 클린업 함수로는 상태 초기화를 수행하여, 컴포넌트가 언마운트되었을 때 상태를 리셋합니다.

6. useEffect의 의존성 배열

useEffect 훅의 두 번째 인자는 의존성 배열입니다. 이 배열은 효과가 재실행될 조건을 설정하는데 사용됩니다. 의존성 배열에 포함된 변수가 변경되면, 해당 effect가 다시 실행됩니다.

useEffect(() => {
    // effect 내용
}, [variable1, variable2]);

위와 같은 형식으로 설정하면, variable1 또는 variable2가 변화할 때마다 effect가 재실행됩니다. 아래에 의존성 배열의 몇 가지 예를 보여드리겠습니다.

예제 1: 상태 변화에 따라 effect 재실행

const TimerComponent = () => {
    const [count, setCount] = useState(0);

    useEffect(() => {
        const timer = setInterval(() => {
            setCount(prevCount => prevCount + 1);
        }, 1000);

        return () => clearInterval(timer);
    }, []);

    return 

Count: {count}

; };

위의 코드에서 setInterval을 통해 1초마다 카운트를 증가시키고 있으며, 의존성 배열이 빈 배열이기 때문에 컴포넌트가 처음 렌더링 될 때만 타이머가 설정됩니다.

예제 2: props 변화에 따른 effect 재실행

const UserProfile = ({ userId }) => {
    const [userData, setUserData] = useState(null);

    useEffect(() => {
        const fetchUserData = async () => {
            const response = await fetch(`https://api.example.com/users/${userId}`);
            const data = await response.json();
            setUserData(data);
        };

        fetchUserData();
    }, [userId]); // userId가 변할 때마다 재실행

    return 
{userData ? userData.name : 'Loading...'}
; };

여기서 userId가 변할 때마다 사용자 데이터가 다시 fetch됩니다. 이는 컴포넌트가 부모 컴포넌트에서 전달받은 props에 따라 렌더링되는 경우 매우 유용합니다.

7. 여러 개의 useEffect 훅 사용하기

리액트에서는 하나의 컴포넌트에서 여러 개의 useEffect 훅을 사용할 수 있습니다. 각 effect는 독립적으로 작동하며, 서로의 의존성에 영향을 미치지 않습니다.

const MultipleEffectsComponent = () => {
    const [value, setValue] = useState(0);
    const [name, setName] = useState('');

    // value 의 변화에 따른 effect
    useEffect(() => {
        console.log('Value changed:', value);
    }, [value]);

    // name 의 변화에 따른 effect
    useEffect(() => {
        console.log('Name changed:', name);
    }, [name]);

    return (
        
setValue(Number(e.target.value))} /> setName(e.target.value)} />
); };

위의 예제에서 valuename 두 개의 상태가 각각의 useEffect 훅에 의해 감지되고, 변화가 발생할 때마다 해당 로그가 출력됩니다.

8. 결론

useEffect 훅은 리액트에서 상태 관리와 비동기 작업을 보다 쉽게 수행할 수 있도록 해줍니다. 컴포넌트의 생명주기를 이해하고, 적절한 타이밍에 효과를 설정하는 것이 매우 중요합니다. 또한, 클린업 함수를 통해 컴포넌트가 언마운트될 때 발생할 수 있는 웹 개발에서의 일반적인 문제를 방지할 수 있습니다. 이 글에서는 useEffect의 사용법과 예제를 중심으로 설명했으며, 이를 통해 비동기 작업과 상태 관리를 효율적으로 구현하는 방법을 배웠습니다. 리액트의 생명주기 관리와 useEffect의 활용이 여러분의 프론트엔드 개발을 더욱 풍부하고 강력하게 만들어줄 것입니다.

9. 추가 학습 자료