[React] useState와 useEffect 활용 및 이벤트 리스너 정리
리액트를 사용하다 보면 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 동작 방식 정리
- 처음 실행될 때 → "창 크기가 바뀌면 실행할 함수"를 등록한다.
- 창 크기가 바뀔 때 → 브라우저(window)가 이벤트 리스너를 감지하고 등록된 함수를 실행한다.
- 컴포넌트가 사라질 때 → useEffect의 cleanup 함수가 실행되어 리스너를 제거한다.
즉, 창 크기를 감지하는 것은 useEffect가 아니라 브라우저(window)이며, useEffect는 단순히 이벤트 리스너를 등록하고 정리하는 역할을 한다.
4. 정리
- useState: 변하는 데이터를 저장하는 훅
- useEffect: 특정 시점(처음 렌더링, 값 변경, 컴포넌트 사라질 때 등)에 실행되는 코드
이 두 가지 훅을 적절히 활용하면 React의 상태 관리와 사이드 이펙트를 효과적으로 처리할 수 있다.