STUDY/React

[React] useState와 useEffect 활용 및 이벤트 리스너 정리

1juyoung 2025. 6. 20. 14:13

리액트를 사용하다 보면 useState와 useEffect를 자주 만나게 되는데 특히, useEffect와 이벤트 리스너가 어떻게 동작하는지 동작 방식에 대해 헷갈리는 경우가 많다. (나만 그랬나..? 🤣)

이런 부분에서 혼란을 느끼는 분들이 있을 거라 생각해서 개념을 쉽게 정리해 보려고 한다!

1. useState - 상태를 저장하는 기본 훅

React에서 상태(state)란 화면에 표시되는 값이 바뀔 수 있는 데이터를 의미한다. 예를 들어, 버튼을 클릭하면 숫자가 증가하거나, 입력창에 텍스트를 입력하면 화면에 반영되는 경우가 있다.

이렇게 변하는 데이터를 저장하고 관리하는 역할을 하는 것이 useState이다.

🔎 기본적인 useState 사용법

import { useState } from "react";

function Counter() {
  const [count, setCount] = useState(0); // 카운터 값을 저장하는 상태

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1 증가</button>
    </div>
  );
}
  • useState(0) → 초기값을 0으로 설정한다.
  • count → 현재 카운트 값을 저장하는 변수
  • setCount → count 값을 변경하는 함수

버튼을 누를 때마다 setCount(count + 1)이 실행되면서 상태가 업데이트되고, 화면이 다시 렌더링된다.

2. useEffect - 특정 시점에 실행되는 코드

React에서 특정 시점에 실행해야 하는 코드가 있을 경우에는 useEffect를 사용한다.

이는 컴포넌트가 처음 렌더링될 때, 특정 값이 변경될 때, 또는 컴포넌트가 사라질 때 필요한 작업을 수행하는 데 **유용하다.

🔎 2-1 기본적인 useEffect 사용법

import { useState, useEffect } from "react";

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

  useEffect(() => {
    console.log(`카운트가 변경됨: ${count}`);
  }, [count]); // count 값이 바뀔 때마다 실행됨

  return (
    <div>
      <p>현재 카운트: {count}</p>
      <button onClick={() => setCount(count + 1)}>+1 증가</button>
    </div>
  );
}
  • useEffect(() => { 실행할 코드 }, [의존성])
  • [count] : count 값이 바뀔 때마다 useEffect가 실행됨

위 코드에서 버튼을 클릭하면 count 값이 변경되고, 콘솔에 "카운트가 변경됨: X"라는 로그가 출력된다.

🔎 2-2 데이터 패칭

import { useEffect, useState } from "react";

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

  useEffect(() => {
    fetch("<https://jsonplaceholder.typicode.com/posts/1>")
      .then(response => response.json())
      .then(json => setData(json));
  }, []); // 빈 배열: 처음 렌더링될 때 한 번만 실행

  return <pre>{JSON.stringify(data, null, 2)}</pre>;

3. useEffect와 이벤트 리스너

useEffect는 이벤트 리스너를 추가하고 정리(cleanup)하는 역할도 할 수 있다.

잘못 작성된 코드와 올바르게 작성된 코드를 통해 자세히 살펴보자면,

3-1 잘못된 코드

function BadExample() {
  window.addEventListener("resize", () => {
    console.log("창 크기 변경됨!");
  });

  return <p>창 크기를 감지하는 컴포넌트</p>;
}

해당 코드의 문제점은 컴포넌트가 렌더링될 때마다 새로운 이벤트 리스너가 계속 추가되며, 컴포넌트가 사라져도 이벤트 리스너가 남아있기 때문에 메모리 누수가 발생할 수 있다는 것이다.

3-2 올바른 코드

import { useEffect } from "react";

function GoodExample() {
  useEffect(() => {
    const handleResize = () => {
      console.log("창 크기가 변경됨!");
    };

    window.addEventListener("resize", handleResize);

    return () => {
      window.removeEventListener("resize", handleResize); // 정리 작업
    };
  }, []);

  return <p>창 크기를 감지하는 컴포넌트</p>;
}
  • window.addEventListener("resize", handleResize); 이벤트 리스너를 등록
  • return () => { window.removeEventListener("resize", handleResize); } 컴포넌트가 사라질 때 리스너 제거

3-3 동작 방식 정리

  1. 처음 실행될 때 → "창 크기가 바뀌면 실행할 함수"를 등록한다.
  2. 창 크기가 바뀔 때 → 브라우저(window)가 이벤트 리스너를 감지하고 등록된 함수를 실행한다.
  3. 컴포넌트가 사라질 때 → useEffect의 cleanup 함수가 실행되어 리스너를 제거한다.

즉, 창 크기를 감지하는 것은 useEffect가 아니라 브라우저(window)이며, useEffect는 단순히 이벤트 리스너를 등록하고 정리하는 역할을 한다.

4. 정리

  • useState: 변하는 데이터를 저장하는 훅
  • useEffect: 특정 시점(처음 렌더링, 값 변경, 컴포넌트 사라질 때 등)에 실행되는 코드

이 두 가지 훅을 적절히 활용하면 React의 상태 관리와 사이드 이펙트를 효과적으로 처리할 수 있다.