React 함수형 컴포넌트에서 부수 효과(side effects)를 관리하는 것은 웹 애플리케이션 개발의 핵심적인 부분입니다. useEffect 훅은 React에서 가장 강력하고 자주 사용되는 도구 중 하나로, 데이터 가져오기, 구독 설정, DOM 직접 조작 등 다양한 부수 효과를 함수형 컴포넌트 안에서 안전하고 효율적으로 처리할 수 있도록 돕습니다. 이 글에서는 useEffect 훅의 기본 개념부터 실용적인 활용법, 그리고 흔히 겪는 문제점과 해결책까지 심도 있게 다루어 보겠습니다. useEffect를 제대로 이해하고 활용하는 것은 React 컴포넌트의 라이프사이클을 효과적으로 관리하고, 애플리케이션의 성능과 안정성을 최적화하는 데 필수적입니다.
useEffect 훅이란 무엇인가요?
useEffect 훅은 함수형 컴포넌트에서 React의 라이프사이클 기능을 사용할 수 있게 해주는 도구입니다. 기존 클래스형 컴포넌트의 componentDidMount, componentDidUpdate, componentWillUnmount와 같은 라이프사이클 메서드를 하나의 훅으로 통합하여 더 간결하고 직관적인 방식으로 부수 효과를 관리할 수 있게 해줍니다.
부수 효과(Side Effects)의 이해
그렇다면 "부수 효과"란 정확히 무엇을 의미할까요? React 컴포넌트 내부에서 외부 세계와 상호 작용하는 모든 작업을 부수 효과라고 할 수 있습니다. 이는 순수한 상태나 props만으로 렌더링이 결정되는 React의 철학과 대비되는 부분으로, 컴포넌트의 렌더링 외부에서 발생하는 변화를 의미합니다. 대표적인 예시는 다음과 같습니다:
- 데이터 가져오기 (Data Fetching): 외부 API로부터 데이터를 요청하거나 전송하는 작업. 이는 가장 흔한 부수 효과 중 하나입니다.
- 구독 (Subscriptions): WebSocket을 통해 실시간 메시지를 받거나, 특정 이벤트에 대한 리스너를 설정하는 작업.
- 타이머 (Timers):
setInterval또는setTimeout과 같이 시간 기반의 작업을 설정하는 작업. - DOM 조작 (DOM Manipulation): React의 일반적인 렌더링 프로세스 외부에서 직접 DOM 요소를 변경하는 작업 (예: 포커스 설정, 스크롤 위치 조작).
이러한 부수 효과를 적절히 관리하지 않으면 메모리 누수나 불필요한 리렌더링이 발생할 수 있으므로, useEffect가 필수적입니다.
useEffect의 기본 구문과 동작 방식
useEffect를 사용하는 기본 구문은 매우 간단합니다.
import React, { useEffect } from 'react';
const MyComponent = () => {
useEffect(() => {
// 여기에 부수 효과 로직을 정의합니다
return () => {
// 선택적으로 정리(cleanup) 코드를 여기에 정의합니다
};
}, [/* 의존성 배열 */]);
return <div>My Component</div>;
};
useEffect 훅은 두 개의 인수를 받습니다:
- 첫 번째 인수는 함수: 이 함수 안에 실제로 수행할 부수 효과 로직을 정의합니다. 이 함수는 컴포넌트가 렌더링된 후에 실행되며, 선택적으로 cleanup 함수를 반환할 수 있습니다.
- 두 번째 인수는 의존성 배열 (Dependency Array): 이 배열은
useEffect가 언제 실행되어야 하는지를 결정합니다. 이 배열이useEffect훅의 가장 중요한 부분 중 하나입니다.
의존성 배열의 역할
의존성 배열은 useEffect의 동작을 정교하게 제어합니다.
- 빈 배열 (
[]):useEffect는 컴포넌트가 처음 렌더링된 후에 단 한 번만 실행됩니다. 이는 클래스형 컴포넌트의componentDidMount와 유사한 동작을 합니다. 주로 초기 데이터 로딩이나 한 번만 필요한 전역 이벤트 리스너 설정 등에 사용됩니다. - 변수 포함 (
[변수1, 변수2, ...]): 배열 안에 포함된 변수들 중 하나라도 값이 변경될 때마다useEffect가 다시 실행됩니다. 이는componentDidUpdate와 유사하며, 특정 상태나 props의 변화에 따라 부수 효과를 재실행해야 할 때 유용합니다. - 의존성 배열 생략: 의존성 배열을 생략하면
useEffect는 컴포넌트가 렌더링될 때마다 (props나 상태가 변경될 때마다) 실행됩니다. 이는 무한 루프나 불필요한 연산을 유발할 수 있으므로, 특별한 경우가 아니라면 의존성 배열을 명시적으로 사용하는 것이 좋습니다.
useEffect의 실용적인 예시
1. 컴포넌트 마운트 시 데이터 가져오기
가장 흔한 useEffect 사용 사례 중 하나는 컴포넌트가 마운트될 때 외부 API로부터 데이터를 가져오는 것입니다.
import React, { useState, useEffect } from 'react';
const UserList = () => {
const [users, setUsers] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('https://jsonplaceholder.typicode.com/users');
const result = await response.json();
setUsers(result);
} catch (error) {
console.error("데이터 가져오기 오류:", error);
}
};
fetchData();
}, []); // 빈 배열: 컴포넌트 마운트 시 한 번만 실행
return (
<div>
<h2>사용자 목록</h2>
{users.length > 0 ? (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
) : (
<p>사용자 데이터를 불러오는 중입니다...</p>
)}
</div>
);
};
export default UserList;
이 예시에서는 useEffect의 두 번째 인수로 빈 배열 []을 사용하여, fetchData 함수가 컴포넌트가 처음 렌더링된 후에만 한 번 실행되도록 합니다. 데이터를 성공적으로 가져오면 setUsers를 통해 상태를 업데이트하고, 이는 컴포넌트의 리렌더링을 유발하여 사용자 목록을 화면에 표시합니다.
2. 특정 상태 변경에 반응하기
useEffect는 특정 상태(state)나 속성(props)의 변화에 따라 동작을 수행해야 할 때도 유용합니다.
import React, { useState, useEffect } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
useEffect(() => {
// count 값이 변경될 때마다 이 로그가 출력됩니다.
console.log(`당신은 ${count}번 클릭했습니다.`);
// 정리(cleanup) 함수: 다음 useEffect 실행 전 또는 컴포넌트 언마운트 시 호출
return () => {
console.log(`count ${count}에 대한 정리 작업`);
};
}, [count]); // count가 변경될 때마다 실행
return (
<div>
<p>클릭 횟수: {count}</p>
<button onClick={() => setCount(count + 1)}>
클릭하세요
</button>
</div>
);
};
export default Counter;
여기서 useEffect는 의존성 배열에 [count]를 포함하고 있습니다. 따라서 count 상태 변수가 변경될 때마다 useEffect 내부의 로직이 다시 실행됩니다. cleanup 함수는 이전 count 값에 대한 로그를 출력하며, 이는 상태 변화 시 이전 효과를 정리하는 데 유용합니다.
3. 부수 효과 정리 (Cleanup)
useEffect의 강력한 기능 중 하나는 부수 효과를 "정리(cleanup)"할 수 있다는 것입니다. 구독 해제, 타이머 해제 등 컴포넌트가 언마운트되거나 다음 useEffect가 실행되기 전에 반드시 수행해야 하는 작업은 useEffect 콜백 함수에서 함수를 반환함으로써 정의할 수 있습니다.
import React, { useEffect, useState } from 'react';
const Timer = () => {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
console.log("타이머 시작!");
const timerId = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// 정리(cleanup) 함수 반환
return () => {
console.log("타이머 정리!");
clearInterval(timerId); // 컴포넌트 언마운트 시 타이머 해제
};
}, []); // 빈 배열: 컴포넌트 마운트 시 한 번만 실행
return (
<div>
<p>경과 시간: {seconds}초</p>
</div>
);
};
export default Timer;
이 예시에서 setInterval로 설정된 타이머는 useEffect 내부에서 시작됩니다. 그리고 반환되는 clearInterval(timerId) 함수는 컴포넌트가 언마운트될 때 (또는 의존성 배열이 변경되어 useEffect가 다시 실행되기 전에) 호출되어 불필요한 타이머 실행을 중지합니다. 이는 메모리 누수나 무한 루프를 방지하는 데 핵심적입니다. 만약 cleanup이 없으면 컴포넌트가 자주 언마운트/마운트될 때 타이머가 쌓여 성능 저하를 초래할 수 있습니다.
흔히 겪는 문제점과 해결책
useEffect를 사용할 때 개발자들이 자주 마주하는 문제들을 살펴보고, 이를 해결하는 방법을 알아보겠습니다. 이러한 문제는 대부분 의존성 배열의 오용이나 클로저 문제에서 비롯됩니다.
1. 무한 루프 (Infinite Loop)
- 문제: 상태 업데이트가
useEffect내부에서 발생하고, 의존성 배열에 그 상태가 포함되어 있으면 리렌더링 → useEffect 재실행 → 상태 업데이트의 무한 반복이 발생합니다. - 해결책: 의존성 배열을 정확히 관리하세요. ESLint의
react-hooks/exhaustive-deps규칙을 활성화하여 누락된 의존성을 경고받을 수 있습니다. 필요 시useCallback훅으로 함수를 메모이제이션하세요.
2. 의존성 배열 누락
- 문제: 배열을 생략하거나 잘못된 값만 포함하면 부수 효과가 예상치 않게 실행되거나 스킵됩니다.
- 해결책: 모든 외부 변수(상태, props, 함수)를 배열에 포함하세요. 객체/배열은 참조 변경이 잦으므로
useMemo나useCallback으로 안정화하세요.
3. 클로저 문제 (Stale Closure)
- 문제:
useEffect가 오래된 상태나 props 값을 캡처하여 최신 값이 반영되지 않습니다. - 해결책: 의존성 배열에 관련 변수를 포함하고, 함수 내부에서
setState의 함수형 업데이트(예:prev => prev + 1)를 사용하세요.
이러한 팁을 따르면 useEffect의 안정성을 크게 높일 수 있습니다.
마무리
useEffect 훅은 React의 함수형 컴포넌트를 더욱 강력하게 만들어주는 도구입니다. 기본 구문을 익히고 의존성 배열을 올바르게 활용하며, cleanup을 잊지 않도록 하세요. 실전에서 다양한 예시를 통해 연습하다 보면 자연스럽게 마스터할 수 있을 것입니다. React 개발을 시작하는 분들에게 이 글이 도움이 되기를 바랍니다!
'프로그래밍 > ReactJS' 카테고리의 다른 글
| React의 강력한 상태 관리 도구: `useReducer` 훅 완벽 가이드 (0) | 2025.10.15 |
|---|---|
| React Hooks: useContext로 깨끗하고 효율적인 상태 관리를 경험하세요! (0) | 2025.10.14 |
| React 개발의 핵심: useState Hook 완전 정복! (0) | 2025.10.14 |
| React 컴포넌트 테스트, Enzyme으로 정복하기: 견고한 애플리케이션의 시작 (0) | 2025.10.14 |
| React Testing Library로 견고한 React 앱 만들기: 사용자 중심 테스트의 힘 (0) | 2025.10.14 |