[React]useState와 useEffect 가이드(Hook) 및 useState와 useEffect의 유의사항

[React]useState와 useEffect 가이드(Hook) 및 useState와 useEffect의 유의사항

1. React 훅(Hook)이란?

React는 함수형 컴포넌트에서 상태(state)와 생명주기(lifecycle) 기능을 활용하기 위해 Hook이라는 개념을 도입했다.

useStateuseEffect는 React의 대표적인 훅으로, 각각 상태 관리부수 효과 관리를 담당한다. 최신 React(버전 18 이상)에서도 훅은 여전히 핵심이며, 컴포넌트 기반 개발에서 필수적으로 사용된다.


2. useState: 상태 관리의 핵심 🔄

2.1 useState란?

useState는 React에서 컴포넌트 상태를 관리하기 위해 사용된다.

컴포넌트의 상태는 렌더링 중에 변경될 수 있는 데이터를 의미하며, React는 상태가 변경될 때마다 UI를 다시 렌더링한다.


2.2 기본 사용법

문법
const [state, setState] = useState(initialValue);
  • state: 현재 상태 값.
  • setState: 상태 값을 업데이트하는 함수.
  • initialValue: 상태의 초기값.
예제
import React, { useState } from 'react';

function Counter() {
    const [count, setCount] = useState(0); // 초기값은 0

    const increment = () => setCount(count + 1);
    const decrement = () => setCount(count - 1);

    return (
        <div>
            <h1>Count: {count}</h1>
            <button onClick={increment}>+</button>
            <button onClick={decrement}>-</button>
        </div>
    );
}

export default Counter;

2.3 상태 업데이트의 특징

  1. 비동기적 업데이트setState는 비동기적으로 동작한다. 즉, 업데이트는 즉시 반영되지 않을 수 있다.
const increment = () => {
    setCount(count + 1);
    console.log(count); // 이전 값 출력 (동기적 기대와 다름)
};
  1. 함수형 업데이트상태 변경에 이전 값을 활용해야 할 경우, 함수형 업데이트를 권장한다.
const increment = () => setCount((prevCount) => prevCount + 1);
  1. 병합되지 않음상태는 객체로 관리할 때 자동 병합되지 않는다. 직접 병합해야 한다.
const [state, setState] = useState({ name: '', age: 0 });
setState({ name: 'Austin' }); // age는 undefined로 덮어씌워짐
setState((prevState) => ({ ...prevState, name: 'Austin' })); // 해결

3. useEffect: 부수 효과 관리의 핵심 🌐

3.1 useEffect란?

useEffect컴포넌트가 렌더링된 이후 또는 상태나 props가 변경된 이후에 실행되는 작업을 정의할 수 있다.

대표적으로 데이터 fetching, DOM 조작, 구독 설정/해제에 사용된다.


3.2 기본 사용법

문법
useEffect(() => {
    // 실행할 작업
    return () => {
        // 정리(cleanup) 작업
    };
}, [dependencies]);
  • 첫 번째 매개변수: 실행할 함수.
  • 두 번째 매개변수 (dependencies): 배열로 지정하며, 의존성이 변경될 때만 실행. 생략 시 매번 렌더링 후 실행.

예제 1: 의존성 없이 실행
import React, { useEffect } from 'react';

function App() {
    useEffect(() => {
        console.log('컴포넌트가 처음 렌더링되었습니다.');
    }, []); // 빈 배열 -> 처음 한 번만 실행

    return <h1>Hello, World!</h1>;
}

export default App;

예제 2: 의존성을 활용한 실행
import React, { useState, useEffect } from 'react';

function Counter() {
    const [count, setCount] = useState(0);

    useEffect(() => {
        console.log(`현재 카운트는 ${count}입니다.`);
    }, [count]); // count가 변경될 때만 실행

    return (
        <div>
            <h1>Count: {count}</h1>
            <button onClick={() => setCount(count + 1)}>+</button>
        </div>
    );
}

export default Counter;

예제 3: 정리 작업

컴포넌트가 언마운트될 때 실행할 작업(예: 구독 해제)을 정의할 수 있다.

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

function Timer() {
    const [seconds, setSeconds] = useState(0);

    useEffect(() => {
        const interval = setInterval(() => {
            setSeconds((prev) => prev + 1);
        }, 1000);

        return () => {
            clearInterval(interval); // 컴포넌트 언마운트 시 정리
        };
    }, []);

    return <h1>{seconds}초 경과</h1>;
}

export default Timer;

3.3 의존성(dependencies) 배열

  1. 빈 배열 ([])
    • 컴포넌트가 처음 렌더링될 때만 실행.
  2. 의존성 배열 생략
    • 상태나 props가 변경될 때마다 실행.
  3. 특정 값 포함
    • 배열에 포함된 값이 변경될 때만 실행.

4. useState와 useEffect를 함께 사용하기 🎯

실제 예제: 데이터 Fetching

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

function UserList() {
    const [users, setUsers] = useState([]);
    const [isLoading, setIsLoading] = useState(true);

    useEffect(() => {
        const fetchUsers = async () => {
            const response = await fetch('<https://jsonplaceholder.typicode.com/users>');
            const data = await response.json();
            setUsers(data);
            setIsLoading(false);
        };

        fetchUsers();
    }, []); // 의존성 없음 -> 처음 렌더링 시 실행

    return (
        <div>
            {isLoading ? (
                <h1>Loading...</h1>
            ) : (
                <ul>
                    {users.map((user) => (
                        <li key={user.id}>{user.name}</li>
                    ))}
                </ul>
            )}
        </div>
    );
}

export default UserList;

5.useEffect에서 useState를 사용하면 발생할 수 있는 문제점🪲

5.1 useState로 상태를 업데이트하면 컴포넌트가 리렌더링됨

useEffect 내부에서 setState를 호출하면 React는 해당 상태 업데이트를 반영하기 위해 컴포넌트를 리렌더링한다. 리렌더링 시 다시 useEffect가 실행될 수 있으므로 무한 루프의 가능성이 있다.

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('useEffect 실행');
    setCount((prevCount) => prevCount + 1); // 상태 업데이트
  }, []); // 의존성 배열에 아무 것도 없으므로 한 번만 실행

  return <div>Count: {count}</div>;
}
결과
  • 컴포넌트가 처음 렌더링될 때 useEffect가 실행되고, setCount를 호출한다.
  • setCount가 상태를 업데이트하면 리렌더링이 발생한다.
  • useEffect는 의존성 배열이 비어 있으므로 처음 렌더링 이후 다시 실행되지 않는다.
  • 무한 루프는 발생하지 않지만, 의도치 않게 상태가 업데이트될 수 있다.

5.2 의존성 배열을 잘못 설정하면 무한 루프 가능성🔁

useEffect의 의존성 배열에 useState로 설정된 상태를 포함하면, 해당 상태가 업데이트될 때마다 useEffect가 실행된다. 만약 useEffect 내부에서 그 상태를 업데이트하면 무한 루프에 빠질 수 있다.

문제 예시
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('Count 업데이트:', count);
    setCount(count + 1); // 상태 업데이트
  }, [count]); // count가 업데이트될 때마다 실행

  return <div>Count: {count}</div>;
}
결과
  • count가 변경되면 useEffect가 실행되고, setCount를 호출한다.
  • setCount가 상태를 변경하면 다시 useEffect가 실행된다.
  • 이 과정이 반복되며 브라우저가 멈추거나, 메모리 초과로 크래시가 발생한다.
해결 방법

무한 루프를 방지하려면 의존성 배열을 신중히 설정하거나, 특정 조건에 따라 상태를 업데이트해야 한다.

useEffect(() => {
  if (count < 5) { // 특정 조건
    setCount(count + 1);
  }
}, [count]);

5.3 상태 업데이트가 비동기적이므로 의도치 않은 동작 가능🚫

React의 상태 업데이트는 비동기적으로 처리된다. useEffect가 실행되는 동안 상태 업데이트가 완료되지 않을 수 있다. 이를 고려하지 않으면 이전 상태값에 의존하는 문제가 생길 수 있다.

문제 예시
import React, { useState, useEffect } from 'react';

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('현재 카운트:', count); // 상태가 즉시 반영되지 않음
    setCount(count + 1); // 의도한 값보다 더 커질 수 있음
  }, []);

  return <div>Count: {count}</div>;
}
해결 방법

상태 업데이트 시 이전 상태를 기반으로 업데이트하려면 콜백 형식을 사용하는 것이 안전하다.

setCount((prevCount) => prevCount + 1);

5.4 useEffect의 의존성 배열이 올바르지 않으면 버그 발생 가능

useState로 관리되는 상태를 useEffect에서 사용하는데 의존성 배열에 포함하지 않으면, 오래된 상태를 참조하거나 예기치 않은 동작이 발생할 수 있다.

문제 예시
useEffect(() => {
  console.log('현재 카운트:', count); // 오래된 값 참조 가능
}, []); // count가 의존성 배열에 없어서 업데이트되지 않음
해결 방법

의존성 배열에 상태를 포함하여 최신 값을 참조하도록 설정해야 한다.

useEffect(() => {
  console.log('현재 카운트:', count);
}, [count]); // count가 변경될 때마다 최신 값으로 실행

5.5 비동기 작업과 상태 업데이트의 조합 문제

useEffect에서 비동기 작업(예: API 호출)을 수행하고 그 결과로 상태를 업데이트할 때, 컴포넌트가 언마운트되면 상태 업데이트가 경고를 발생시킬 수 있다.

문제 예시
useEffect(() => {
  fetch('/api/data')
    .then((response) => response.json())
    .then((data) => setState(data)); // 컴포넌트가 언마운트된 상태에서 업데이트 시 문제
}, []);
해결 방법

비동기 작업을 취소하거나, 상태 업데이트 여부를 컴포넌트의 마운트 상태에 따라 결정해야 한다.

useEffect(() => {
  let isMounted = true;

  fetch('/api/data')
    .then((response) => response.json())
    .then((data) => {
      if (isMounted) setState(data);
    });

  return () => {
    isMounted = false; // 언마운트 시 상태 플래그 변경
  };
}, []);

6. 주요 차이점과 유의 사항

useState vs useEffect

  • useState: 상태를 저장하고 업데이트하는 역할.
  • useEffect: 상태 변경이나 렌더링 후의 부수 효과를 처리.

유의사항

  1. 의존성 배열 관리: 불필요한 재실행 방지.
  2. 정리 작업 명시: 메모리 누수 방지를 위해 정리(cleanup) 필수.
  3. 비동기 작업: useEffect에서 비동기 함수는 내부에 정의하고 호출.
  4. 무한루프: useEffect에서 useState를 사용할 때 리렌더링이 발생하며, 의존성 배열을 잘못 설정하면 무한 루프 문제가 생길 수 있다.
  5. 의존성: 상태 업데이트는 비동기적으로 작동하므로 이전 상태에 의존하는 경우 주의해야 한다.
  6. 추가적인 처리 필요: 비동기 작업이나 외부 API 호출과 상태 업데이트를 결합할 때는 메모리 누수나 경고를 방지하기 위해 추가 처리가 필요하다.

7. 결론

useStateuseEffect는 React 컴포넌트의 상태와 생명주기를 효율적으로 관리하기 위한 핵심 훅이다.

  • useState는 상태 관리를 단순화하고,
  • useEffect는 컴포넌트의 부수 효과를 선언적으로 정의할 수 있다.

댓글