리액트를 공부하다 보면 useEffect는 거의 반드시 만나게 됩니다. 문제는 많이 쓰인다는 이유로 아무 데나 넣기 쉽다는 점입니다. 초보자일수록 “값이 바뀌면 뭔가 해야 하니까 useEffect”라는 식으로 접근하다가, 무한 루프나 불필요한 재렌더링을 자주 경험합니다.
useEffect는 강력하지만, 동시에 React 코드가 복잡해지는 가장 흔한 출발점이기도 합니다. 그래서 문법보다 먼저 “이 훅이 정말 필요한 상황이 무엇인가”를 구분하는 훈련이 중요합니다.
이 글에서는 아래 내용을 정리합니다.
useEffect가 필요한 경우와 필요 없는 경우- 의존성 배열을 어떻게 읽어야 하는지
- 흔한 실수와 더 단순한 대안
핵심은 useEffect는 렌더링 결과 바깥 세계와 동기화할 때 쓰는 도구라는 점입니다.
React useEffect는 무엇인가
useEffect는 컴포넌트가 렌더링된 뒤 실행되는 작업을 등록하는 훅입니다. 보통 아래 같은 상황에서 사용합니다.
- API 요청 보내기
- 이벤트 리스너 등록하기
- 타이머 시작하거나 정리하기
- 외부 라이브러리와 상태 맞추기
- 브라우저 API와 동기화하기
예를 들어 페이지가 열릴 때 데이터를 가져오는 코드는 아래처럼 쓸 수 있습니다.
useEffect(() => {
async function fetchPosts() {
const response = await fetch('/api/posts');
const data = await response.json();
setPosts(data);
}
fetchPosts();
}, []);
의존성 배열 []는 이 effect가 마운트 시점에 한 번 실행되는 구조임을 보여줍니다.
언제 useEffect가 필요한가
가장 쉬운 기준은 “렌더링만으로 해결되지 않는가”입니다. 아래 상황이라면 useEffect가 자연스럽습니다.
- 외부 데이터 소스와 연결해야 한다
- DOM 이벤트를 직접 다뤄야 한다
- 타이머나 구독을 정리해야 한다
- React 밖의 상태와 값을 맞춰야 한다
예를 들어 스크롤 이벤트를 구독해야 한다면 effect가 필요합니다.
useEffect(() => {
function handleScroll() {
console.log(window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
이처럼 등록과 해제가 함께 필요한 경우는 useEffect의 전형적인 사용처입니다.
언제 useEffect를 피하는 편이 좋은가
반대로 아래 상황은 effect 없이도 풀 수 있는 경우가 많습니다.
- props나 state로부터 계산 가능한 값 만들기
- 클릭 이벤트가 일어났을 때만 처리하면 되는 로직
- 렌더링 중 바로 계산해도 되는 파생 값
- 단순한 폼 입력 처리
예를 들어 전체 가격 합계를 굳이 effect로 만들 필요는 없습니다.
const totalPrice = items.reduce((sum, item) => sum + item.price, 0);
이런 계산은 렌더링 안에서 바로 처리하는 편이 더 단순합니다. effect로 setTotalPrice를 따로 두면 오히려 state가 하나 더 생기고, 동기화 문제도 늘어납니다.
의존성 배열은 어떻게 이해해야 할까
의존성 배열은 “이 effect가 어떤 값에 반응해야 하는가”를 선언하는 곳입니다. effect 내부에서 사용하는 값 중 렌더링마다 바뀔 수 있는 값은 보통 의존성에 포함되어야 합니다.
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
여기서는 count가 바뀔 때마다 문서 제목을 다시 맞춰야 하므로 [count]가 자연스럽습니다.
의존성 배열을 억지로 비워서 경고를 없애는 방식은 대체로 좋지 않습니다. 경고가 뜬다면 정말 한 번만 실행되어야 하는지, 아니면 로직 자체를 다른 위치로 옮겨야 하는지를 먼저 점검하는 편이 좋습니다.
가장 흔한 실수
1. effect 안에서 파생 state 만들기
filteredItems, fullName, isFormValid처럼 기존 값으로 계산 가능한 값을 effect로 저장하면 코드가 길어지고 상태 동기화 부담이 커집니다.
2. 이벤트 로직을 effect로 옮기기
버튼 클릭 후 실행할 로직은 보통 클릭 핸들러 안에 두는 편이 자연스럽습니다. 이벤트로 충분한데 effect까지 쓰면 흐름이 우회됩니다.
3. cleanup을 잊기
이벤트 리스너, interval, subscription은 정리 함수가 빠지면 메모리 누수나 중복 실행 문제가 생기기 쉽습니다.
4. fetch와 로딩 상태를 매번 직접 쓰기
작은 예제에서는 괜찮지만, 앱이 커지면 로딩, 에러, 재시도, 캐싱이 반복됩니다. 이 시점부터는 custom hook이나 서버 상태 라이브러리를 고려할 만합니다.
실전 감각을 키우는 질문
useEffect를 쓰기 전에 아래를 먼저 자문해보세요.
- 이 로직은 외부 세계와 동기화하는 일인가?
- 단순 계산이라면 렌더링 안에서 바로 할 수 없는가?
- 특정 이벤트에서만 필요한 일인가?
- cleanup이 필요한 작업인가?
- 같은 패턴이 반복된다면 hook으로 뽑을 수 없는가?
이 질문만 습관화해도 useEffect 남용이 크게 줄어듭니다.
FAQ
Q. 데이터 요청은 무조건 useEffect로 해야 하나요?
작은 예제에서는 흔한 방식이지만, 프레임워크나 데이터 패칭 라이브러리에 따라 더 나은 방법이 있을 수 있습니다.
Q. 의존성 배열 경고가 뜨면 그냥 무시해도 되나요?
보통은 무시하지 않는 편이 좋습니다. 경고는 흐름이 꼬일 가능성을 알려주는 경우가 많습니다.
Q. useEffect가 많아질수록 나쁜 코드인가요?
반드시 그렇지는 않지만, effect가 많아질수록 외부 동기화 지점이 늘어난다는 뜻이므로 구조를 다시 볼 가치가 있습니다.
Read Next
- 반복되는 effect 패턴을 정리하고 싶다면 React 커스텀 훅 가이드를 이어서 읽어보세요.
- 렌더링 비용과 메모이제이션 기준이 궁금하다면 React 렌더링 최적화 가이드가 다음 단계에 잘 맞습니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.
먼저 읽어볼 가이드
검색 유입이 많은 핵심 글부터 이어서 보세요.
- 미들웨어 트러블슈팅 가이드: Redis vs RabbitMQ vs Kafka 개발자를 위한 미들웨어 트러블슈팅 허브 글입니다. Redis, RabbitMQ, Kafka 중 어떤 증상부터 먼저 봐야 하는지와 어떤 문제 패턴이 각 시스템에 가까운지 정리합니다.
- Kubernetes CrashLoopBackOff: 먼저 볼 것들 startup failure, probe, config, resource limit 관점에서 CrashLoopBackOff를 어떻게 나눠서 봐야 하는지 정리한 가이드입니다.
- Kafka consumer lag가 계속 늘 때: 트러블슈팅 가이드 Kafka consumer lag가 계속 늘어날 때 무엇부터 봐야 하는지 정리합니다. poll 주기, 처리 속도, rebalance, consumer 설정까지 실전 기준으로 다룹니다.
- Kafka Rebalancing Too Often 가이드 Kafka consumer group에서 rebalance가 너무 자주 일어날 때 membership flapping, poll timing, protocol, assignment churn을 어떤 순서로 봐야 하는지 설명하는 실전 가이드입니다.
- Docker container가 계속 재시작될 때: 먼저 확인할 것들 exit code, command failure, environment mistake, health check 관점에서 Docker restart loop를 푸는 실전 가이드입니다.
심사 대기 중에는 광고 대신 관련 가이드를 먼저 보여줍니다.