React useEffect 완벽 가이드 외부 API 호출과 생명주기 이해
⚡ 부수 효과를 관리하는 핵심 Hook useEffect 완벽 정리
리액트를 처음 접하거나 프로젝트를 진행하면서 가장 많이 부딪히는 부분이 바로 컴포넌트의 생명주기와 상태 관리입니다.
특히 외부 API 호출, 이벤트 리스너 등록, 타이머와 같은 부수 효과 처리는 한 번만 실행해야 하는지, 특정 조건에서만 실행해야 하는지 헷갈리기 쉽죠.
이런 상황에서 핵심 역할을 하는 것이 바로 useEffect Hook입니다.
컴포넌트의 렌더링 주기와 의존성 배열을 기반으로 실행 조건을 제어할 수 있기 때문에, 효율적이고 깔끔한 코드를 작성하는 데 필수적으로 사용됩니다.
이번 글에서는 React useEffect의 기본 개념부터 API 호출, 구독 설정, 타이머 관리 등 실전 예제까지 단계별로 살펴봅니다.
복잡한 개념을 단순히 이론으로 설명하지 않고, 초보자도 바로 적용할 수 있도록 실용적인 코드 예시와 함께 정리했습니다.
이 글을 읽고 나면 useEffect를 언제, 어떻게 활용해야 할지 확실하게 이해할 수 있을 것입니다.
📋 목차
🔗 useEffect 기본 개념 이해하기
리액트에서 useEffect는 함수형 컴포넌트가 외부와 상호작용할 수 있도록 돕는 핵심 Hook입니다.
렌더링이 일어날 때마다 특정 동작을 실행하거나, 조건을 걸어 필요할 때만 실행되도록 설정할 수 있습니다.
이는 클래스형 컴포넌트에서 사용되던 componentDidMount, componentDidUpdate, componentWillUnmount의 역할을 대체합니다.
예를 들어 API에서 데이터를 불러와 화면에 표시해야 한다면, 컴포넌트가 처음 렌더링될 때 한 번만 실행되도록 설정할 수 있습니다.
또한 상태가 변경될 때마다 서버에 알림을 보내거나, 스크롤 이벤트를 감지하는 등의 작업도 useEffect로 처리할 수 있습니다.
이러한 작업들은 컴포넌트가 단순히 UI를 그리는 것을 넘어 실제 동작을 제어하기 때문에 부수 효과(side effect)라고 부릅니다.
📌 기본 문법 구조
import { useEffect } from "react";
function MyComponent() {
useEffect(() => {
console.log("컴포넌트가 마운트되거나 업데이트됨");
return () => {
console.log("컴포넌트가 언마운트되거나 업데이트 직전");
};
});
return <div>Hello World</div>;
}
위 코드에서 보듯이 useEffect는 첫 번째 인자로 콜백 함수를 받습니다.
이 콜백 함수 안에서는 원하는 동작을 실행할 수 있으며, 반환하는 함수는 클린업(clean-up) 역할을 합니다.
클린업은 메모리 누수 방지나 이벤트 리스너 해제 같은 뒷정리를 수행합니다.
💎 핵심 포인트:
useEffect는 함수형 컴포넌트가 외부 세계와 연결될 수 있는 창구입니다. 생명주기 메서드를 대체하면서도 더 직관적이고 유연한 방식으로 부수 효과를 관리할 수 있습니다.
🛠️ 의존성 배열과 실행 조건
useEffect의 가장 중요한 특징 중 하나는 의존성 배열입니다.
의존성 배열은 useEffect의 두 번째 인자로 전달되며, 이 배열의 값이 변경될 때마다 해당 effect가 다시 실행됩니다.
이 배열을 어떻게 설정하느냐에 따라 실행 횟수와 시점이 달라지므로, 올바른 이해가 필요합니다.
📌 의존성 배열 사용 예시
// 1. 의존성 배열 없음 → 매 렌더링마다 실행
useEffect(() => {
console.log("항상 실행됩니다.");
});
// 2. 빈 배열 [] → 컴포넌트 마운트 시 1회만 실행
useEffect(() => {
console.log("처음 한 번만 실행됩니다.");
}, []);
// 3. 특정 상태값 [count] → 해당 값이 변할 때만 실행
useEffect(() => {
console.log("count 값이 변할 때 실행됩니다.");
}, [count]);
위 예시에서 보듯이, 의존성 배열을 어떻게 설정하느냐에 따라 실행 시점이 크게 달라집니다.
배열을 비워 두면 마운트 시 한 번만 실행되므로 API 요청 같은 초기 작업에 적합합니다.
반대로 특정 상태나 props를 의존성 배열에 넣으면 해당 값이 변할 때만 실행되므로, 효율적인 렌더링 제어가 가능합니다.
📌 주의해야 할 점
⚠️ 주의: 의존성 배열에 함수를 직접 넣으면 무한 렌더링이 발생할 수 있습니다.
useCallback이나 useMemo로 함수를 메모이제이션 처리한 뒤 넣어야 안전합니다.
의존성 배열을 올바르게 관리하지 않으면 불필요한 네트워크 요청이나 성능 저하로 이어질 수 있습니다.
따라서 상태값, props, 메모이제이션 여부 등을 꼼꼼히 확인한 뒤 배열에 넣는 것이 바람직합니다.
- ✅API 요청은 보통 빈 배열 []로 설정
- ✅특정 상태 감시는 해당 값을 배열에 추가
- ✅불필요한 렌더링 방지를 위해 useCallback 적극 활용
⚙️ API 호출과 비동기 처리
리액트 애플리케이션에서 API 호출은 가장 빈번하게 발생하는 부수 효과 중 하나입니다.
useEffect를 활용하면 컴포넌트가 마운트될 때 서버로부터 데이터를 가져오거나, 특정 상태가 변할 때 데이터를 새로 요청하는 로직을 손쉽게 관리할 수 있습니다.
특히 의존성 배열과 함께 사용하면 불필요한 중복 요청을 줄이고 성능을 최적화할 수 있습니다.
📌 기본 API 호출 예제
import { useEffect, useState } from "react";
function PostList() {
const [posts, setPosts] = useState([]);
useEffect(() => {
async function fetchData() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const data = await response.json();
setPosts(data);
}
fetchData();
}, []); // 마운트 시 한 번만 실행
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}
위 코드는 컴포넌트가 처음 렌더링될 때 API를 호출하고, 결과를 상태로 저장하여 화면에 출력합니다.
의존성 배열을 빈 배열로 설정했기 때문에 마운트 시 단 한 번만 실행됩니다.
이 방식은 초기 데이터 로딩에 가장 적합합니다.
📌 상태 기반으로 API 재호출
const [userId, setUserId] = useState(1);
const [user, setUser] = useState(null);
useEffect(() => {
async function fetchUser() {
const res = await fetch(`https://jsonplaceholder.typicode.com/users/${userId}`);
const data = await res.json();
setUser(data);
}
fetchUser();
}, [userId]); // userId가 변경될 때마다 실행
위 예시는 userId 값이 변경될 때마다 해당 사용자 정보를 다시 불러옵니다.
즉, 의존성 배열에 userId를 넣어주면, 해당 값이 변할 때만 API 호출이 실행되므로 불필요한 네트워크 낭비를 방지할 수 있습니다.
💬 비동기 함수는 useEffect 내부에서 직접 선언하고 실행하는 방식이 권장됩니다.
외부에서 정의한 함수를 바로 호출하면 의존성 배열 관리가 복잡해질 수 있기 때문입니다.
🔌 이벤트 리스너와 구독 관리
useEffect는 단순히 API 호출에만 사용되는 것이 아니라, 이벤트 리스너 등록이나 구독(subscription) 관리에도 유용합니다.
예를 들어 브라우저 창 크기 변경을 감지하거나, WebSocket을 통해 실시간 데이터를 주고받을 때도 useEffect를 사용할 수 있습니다.
중요한 점은 이런 구독은 반드시 클린업 함수에서 해제해야 메모리 누수를 막을 수 있다는 것입니다.
📌 윈도우 이벤트 리스너 등록
import { useEffect, useState } from "react";
function WindowSize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return <p>현재 창 너비: {width}px</p>;
}
위 코드에서는 브라우저 창 크기가 변경될 때마다 상태가 업데이트됩니다.
컴포넌트가 언마운트될 때 removeEventListener를 실행해 이벤트 리스너를 제거하므로 불필요한 리스너 누적을 방지할 수 있습니다.
📌 WebSocket 구독 관리
useEffect(() => {
const socket = new WebSocket("wss://example.com/socket");
socket.onmessage = (event) => {
console.log("새로운 메시지:", event.data);
};
return () => {
socket.close(); // 언마운트 시 구독 해제
};
}, []);
이처럼 실시간 데이터를 다루는 경우, 구독을 설정한 뒤 반드시 클린업 단계에서 해제해 주어야 합니다.
그렇지 않으면 컴포넌트가 사라진 후에도 네트워크 연결이 유지되어 성능 문제나 예기치 못한 동작이 발생할 수 있습니다.
💎 핵심 포인트:
이벤트 리스너와 구독은 반드시 클린업 함수에서 해제해야 안전합니다. 그렇지 않으면 메모리 누수, 중복 실행, 성능 저하가 발생할 수 있습니다.
💡 타이머와 클린업 활용
리액트에서 setTimeout이나 setInterval 같은 타이머 함수도 부수 효과에 해당합니다.
useEffect 내부에서 타이머를 설정하고, 컴포넌트가 언마운트되거나 의존성이 변경될 때 타이머를 정리하는 방식으로 관리해야 합니다.
이 과정을 통해 불필요한 반복 실행을 막고 성능을 지킬 수 있습니다.
📌 setInterval 예제
import { useEffect, useState } from "react";
function Clock() {
const [time, setTime] = useState(new Date());
useEffect(() => {
const timer = setInterval(() => {
setTime(new Date());
}, 1000);
return () => clearInterval(timer); // 클린업
}, []);
return <p>현재 시간: {time.toLocaleTimeString()}</p>;
}
위 코드는 1초마다 시간을 갱신하는 간단한 시계 예제입니다.
컴포넌트가 언마운트되면 clearInterval을 호출하여 타이머를 해제합니다.
이 과정이 없으면 계속 실행되면서 메모리 누수나 불필요한 연산이 발생할 수 있습니다.
📌 setTimeout 예제
function Notification() {
const [visible, setVisible] = useState(true);
useEffect(() => {
const timer = setTimeout(() => {
setVisible(false);
}, 3000);
return () => clearTimeout(timer);
}, []);
return visible ? <div>알림 메시지</div> : null;
}
위 예시는 알림 메시지를 3초 동안만 표시한 뒤 자동으로 사라지게 하는 방법입니다.
마찬가지로 언마운트 시 clearTimeout으로 타이머를 해제해야 불필요한 동작을 막을 수 있습니다.
💡 TIP: 타이머는 꼭 클린업 함수에서 제거하세요. 그렇지 않으면 컴포넌트가 사라진 뒤에도 계속 실행되어 성능 저하를 일으킬 수 있습니다.
❓ 자주 묻는 질문 (FAQ)
useEffect는 언제 실행되나요?
빈 배열 []을 넣으면 어떤 효과가 있나요?
클린업 함수는 언제 호출되나요?
useEffect 안에서 비동기 함수를 직접 사용할 수 있나요?
의존성 배열에 함수를 넣으면 문제가 되나요?
useEffect를 여러 번 사용할 수 있나요?
useLayoutEffect와의 차이는 무엇인가요?
useEffect를 잘못 사용하면 어떤 문제가 생기나요?
📌 useEffect로 React 생명주기 완벽 이해하기
지금까지 useEffect의 개념, 의존성 배열, API 호출, 이벤트 구독, 타이머 관리 등 다양한 활용법을 살펴보았습니다.
useEffect는 단순히 부수 효과를 처리하는 도구가 아니라, 함수형 컴포넌트의 생명주기를 유연하고 명확하게 제어할 수 있는 강력한 Hook입니다.
의존성 배열을 올바르게 설정하고, 클린업 함수를 통해 자원을 정리하는 습관만 잘 들이면 대부분의 부수 효과를 안정적으로 처리할 수 있습니다.
특히 실제 프로젝트에서는 API 통신, WebSocket 구독, 이벤트 리스너 등록 같은 상황이 빈번하게 발생합니다.
이때 useEffect를 적절히 활용하면 불필요한 리소스 낭비를 막고, 성능 저하 없이 안정적인 사용자 경험을 제공할 수 있습니다.
즉, useEffect를 이해하는 것은 곧 리액트 컴포넌트를 제대로 다루는 핵심 역량이라고 할 수 있습니다.
🏷️ 관련 태그 : React, useEffect, 리액트생명주기, 리액트훅, 프론트엔드개발, 자바스크립트, API호출, 이벤트리스너, 클린업, 타이머관리