Front-end/React

[React] useEffect: 라이프 사이클 관리자 🔮

xeunnie 2024. 12. 9. 01:00
728x90
반응형

React의 useEffect 톺아보기! 🔍

useEffect는 React에서 함수형 컴포넌트의 라이프사이클비동기 작업을 관리하는 데 사용되는 강력한 Hook입니다. 이 글에서는 useEffect의 기본 개념부터 고급 사용법, 동작 원리, 그리고 실무에서 마주할 수 있는 문제 해결 방법까지 다뤄보겠습니다.


useEffect란 무엇인가요? 🤔

useEffect는 React의 함수형 컴포넌트에서 컴포넌트가 렌더링된 이후 실행되는 작업을 정의할 수 있는 Hook이자, 부수 효과(side effects)를 처리하기 위해 사용됩니다.
 

부수 효과란?

  • 서버에서 데이터를 가져오기
  • 브라우저 타이틀 업데이트
  • 이벤트 리스너 추가 및 정리
  • 타이머 설정 및 정리

 
클래스형 컴포넌트에서는 componentDidMount, componentDidUpdate, componentWillUnmount로 나뉘어 처리하던 작업을 함수형 컴포넌트에서는 useEffect 하나로 통합해서 처리할 수 있습니다.


useEffect의 기본 문법 📚

useEffect(() => {
  // 실행할 코드
  return () => {
    // Cleanup (선택 사항)
  };
}, [의존성 배열]);

매개변수

  1. Effect 함수: 실행할 작업(콜백 함수)
  2. 의존성 배열: useEffect가 실행되는 조건을 지정. 생략 시 매번 렌더링될 때 실행됩니다.

useEffect의 기본 사용법 🛠️

기본 예제: 컴포넌트가 렌더링될 때 실행하기

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

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

  useEffect(() => {
    console.log(`You clicked ${count} times`);
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

실행 순서

  1. 컴포넌트가 렌더링된 후 useEffect 실행
  2. 상태가 변경되면 다시 렌더링 → useEffect 다시 실행.

useEffect의 동작 원리 ⚙️

  1. 렌더링 이후 실행
    • 컴포넌트가 렌더링되고 DOM이 업데이트된 후에 실행
  2. 의존성 배열을 기반으로 조건부 실행
    • 배열 내의 값이 변경될 때만 실행
  3. Cleanup 함수 실행
    • 컴포넌트가 제거되거나, 다음 Effect 실행 전에 정리(clean-up) 작업을 실행

useEffect의 의존성 배열: 정말 중요한 개념! 🧠

 

의존성 배열은 useEffect가 언제 실행될지 결정하는 역할을 합니다.useEffect가 실행되는 시점을 제어할 수 있어요.
 

1. 의존성 배열이 없는 경우

  • 컴포넌트가 렌더링될 때마다 실행됩니다.
useEffect(() => {
  console.log("컴포넌트가 렌더링될 때마다 실행");
});

 

2. 의존성 배열이 있는 경우

  • count가 변경될 때만 실행됩니다.
useEffect(() => {
  console.log("count가 변경될 때만 실행됩니다!");
}, [count]);

 

3. 빈 배열([])

  • 마운트 시 한 번만 실행됩니다.
useEffect(() => {
  console.log("처음 렌더링 시에만 실행");
}, []);

 

4. 특정 상태나 props에 의존

  • 배열에 명시된 값이 변경될 때만 실행됩니다.
useEffect(() => {
  console.log("count가 변경될 때 실행");
}, [count]);

 

5. 복잡한 의존성 관리

  • 여러 상태나 props를 의존성으로 설정 가능.
useEffect(() => {
  console.log("count나 userName이 변경될 때 실행");
}, [count, userName]);

useEffect와 Cleanup 🧹

useEffect는 정리(cleanup) 함수를 반환할 수 있어요. Effect로 인해 발생한 부작용을 정리하는 데 사용됩니다. 이를 통해 타이머나 이벤트 리스너 같은 리소스를 정리할 수 있습니다.
 

언제 실행되나요?

  1. 컴포넌트가 제거될 때.
  2. 다음 Effect가 실행되기 전에.

 

Cleanup 예제: 타이머 정리

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); // 타이머 정리
      console.log("Timer cleaned up!");
    };
  }, []);

  return <p>Elapsed time: {seconds} seconds</p>;
}

Cleanup이 필요한 이유

  • 메모리 누수 방지
  • 불필요한 리소스 소모 방지

useEffect의 실무 활용 🐾

1. API 호출 및 데이터 가져오기(Fetching Data)

서버에서  데이터를 가져오는 작업은 useEffect의 대표적인 활용 사례로 가장 자주 사용됩니다.

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

function FetchData() {
  const [data, setData] = useState([]);

  useEffect(() => {
    async function fetchData() {
      const response = await fetch("https://jsonplaceholder.typicode.com/posts");
      const result = await response.json();
      setData(result);
    }
    fetchData();
  }, []); // 빈 배열로 첫 렌더링 시에만 실행

  return (
    <ul>
      {data.map((item) => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

 

2. 브라우저 타이틀 업데이트

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

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

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  }, [count]); // count가 변경될 때만 실행

  return <button onClick={() => setCount(count + 1)}>Click me</button>;
}

3. 이벤트 리스너 관리

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

function MouseTracker() {
  const [position, setPosition] = useState({ x: 0, y: 0 });

  useEffect(() => {
    const handleMouseMove = (event) => {
      setPosition({ x: event.clientX, y: event.clientY });
    };

    window.addEventListener("mousemove", handleMouseMove);

    return () => {
      window.removeEventListener("mousemove", handleMouseMove); // 정리
    };
  }, []);

  return (
    <p>
      Mouse position: {position.x}, {position.y}
    </p>
  );
}

 

4. 로컬 스토리지와 상태 동기화

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

function LocalStorageSync() {
  const [name, setName] = useState(() => localStorage.getItem("name") || "");

  useEffect(() => {
    localStorage.setItem("name", name);
  }, [name]); // name이 변경될 때만 실행

  return (
    <input
      type="text"
      value={name}
      onChange={(e) => setName(e.target.value)}
      placeholder="Enter your name"
    />
  );
}

useEffect에서 주의할 점 ⚠️

 

1.  의존성 배열 관리

  • 의존성 배열을 생략하면 매 렌더링마다 실행됩니다.
  • 의존성 배열이 불완전하면 예상치 못한 동작이 발생할 수 있습니다.
  • ESLint 플러그인은 누락된 의존성을 경고해줍니다.
  • 의존성 배열을 잘못 설정하면 무한 루프가 발생할 수 있어요.
useEffect(() => {
  setState(state + 1); // 무한 루프
}, [state]); // 매번 상태가 변경되어 다시 실행

2. 동기화 문제

    • useEffect는 렌더링 이후에 실행되므로, 초기 렌더링 중 데이터를 사용할 경우 렌더링이 두 번 발생할 수 있습니다.
    • useEffect 내부에서 비동기 함수를 직접 사용하면 예기치 못한 동작이 발생할 수 있습니다. 대신, 내부에 async 함수를 선언하세요.
useEffect(() => {
  async function fetchData() {
    const data = await someAPI();
    console.log(data);
  }
  fetchData();
}, []);

3. 의존성 배열의 객체 비교

    • 객체나 배열은 참조 값이 변경될 수 있으므로, 이를 의존성으로 사용할 때는 주의가 필요합니다.
    • useEffect가 실행될 때마다 Cleanup이 실행되므로, 불필요한 정리가 발생하지 않도록 신중해야 합니다.

useEffect로 구현한 종합 예제 🎯

할 일 관리 앱에서의 useEffect

  • API를 통해 초기 데이터를 가져오고, 상태를 저장하며, 컴포넌트가 제거될 때 정리 작업을 수행.
import React, { useState, useEffect } from "react";

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [task, setTask] = useState("");
  useEffect(() => {
    // 초기 데이터 가져오기
    async function fetchTodos() {
      const response = await fetch("https://jsonplaceholder.typicode.com/todos");
      const data = await response.json();
      setTodos(data.slice(0, 10)); // 10개만 가져오기
    }
    fetchTodos();
    // Cleanup (여기선 예제 용도로만 작성)
    return () => {
      console.log("컴포넌트가 제거되었습니다.");
    };
  }, []);

  const addTask = () => {
    setTodos((prev) => [...prev, { id: Date.now(), title: task, completed: false }]);
    setTask("");
  };

  return (
    <div>
      <h1>Todo List</h1>
      <input value={task} onChange={(e) => setTask(e.target.value)} placeholder="Add a task" />
      <button onClick={addTask}>Add</button>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>
            <span>{todo.title}</span>
          </li>
        ))}
      </ul>
    </div>
  );
}

 

다중 상태 관리 및 API 호출

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

function WeatherApp() {
  const [location, setLocation] = useState("Seoul");
  const [weather, setWeather] = useState(null);
  const [error, setError] = useState(null);

  useEffect(() => {
    async function fetchWeather() {
      try {
        const response = await fetch(
          `https://api.weatherapi.com/v1/current.json?key=YOUR_API_KEY&q=${location}`
        );
        if (!response.ok) throw new Error("Failed to fetch weather data");
        const data = await response.json();
        setWeather(data);
      } catch (err) {
        setError(err.message);
      }
    }

    fetchWeather();
  }, [location]);

  return (
    <div>
      <h1>Weather App</h1>
      <input
        value={location}
        onChange={(e) => setLocation(e.target.value)}
        placeholder="Enter location"
      />
      {error && <p>Error: {error}</p>}
      {weather ? (
        <div>
          <h2>{weather.location.name}</h2>
          <p>{weather.current.temp_c}°C</p>
          <p>{weather.current.condition.text}</p>
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

결론 🏁

useEffect는 단순히 컴포넌트의 라이프사이클을 관리하는 것을 넘어, 비동기 작업, 이벤트 리스너, 상태 동기화 등을 모두 처리할 수 있는 강력한 도구입니다. 잘 이해하고 적절히 사용하면, React 애플리케이션의 성능과 유지보수성을 한 단계 끌어올릴 수 있습니다.
 

🌷전설의 개발자가 되어봅시당! 🌷

728x90
반응형