useEffect — 렌더링 바깥의 일을 다루기
React의 useEffect를 '사이드 이펙트'라는 개념부터 짚습니다. 렌더링과 무엇이 다른지, 의존성 배열의 세 가지 형태가 각각 언제 실행되는지, 그리고 흔한 오해까지 정리합니다.
지난 글 끝에서 이런 질문을 남겼습니다. 상태가 바뀔 때 화면을 그리는 것 말고 다른 일을 하고 싶다면? 검색어가 바뀔 때마다 서버에 요청을 보낸다든지 말입니다. 그 ‘화면 밖의 일’을 다루는 훅이 useEffect입니다.
useState가 “상태를 바꿔 화면을 갱신”하는 쪽이라면, useEffect는 반대로 “상태나 렌더링에 맞춰 어떤 동작을 수행”하는 쪽입니다.
사이드 이펙트란
1편에서 컴포넌트는 “상태를 받아 화면을 반환하는 함수”라고 했습니다. 이 반환, 즉 렌더링은 순수해야 합니다. 같은 입력이면 같은 화면을 내놓고, 그 과정에서 바깥 세계를 건드리지 않아야 예측 가능하니까요.
하지만 실제 앱은 바깥 세계와 끊임없이 얽힙니다. 서버에서 데이터를 가져오고, 타이머를 켜고, 브라우저 이벤트를 구독하고, 로그를 남깁니다. 이렇게 렌더링의 순수한 계산 바깥에서 벌어지는 일을 사이드 이펙트(side effect)라고 부릅니다. useEffect는 이런 일들을 렌더링 흐름에서 떼어내, 화면이 그려진 뒤에 안전하게 실행해 주는 도구입니다.
import { useEffect } from 'react';
기본 문법
useEffect(() => {
// 여기서 사이드 이펙트를 수행
}, [의존성]);
인자는 두 개입니다.
- 첫 번째: 실행할 함수(설정 함수)
- 두 번째: 의존성 배열 — 이 이펙트가 언제 다시 실행될지를 결정하는 값들의 목록
핵심은 이겁니다. 이펙트는 렌더링 도중이 아니라 화면이 그려진 직후에 실행됩니다. 그리고 매번이 아니라, 의존성 배열의 값이 바뀌었을 때만 다시 실행됩니다.
의존성 배열이 전부다
useEffect의 동작은 결국 두 번째 인자를 어떻게 두느냐로 갈립니다. 세 가지 경우가 있습니다.
// 1) [값] — 첫 렌더 후 1회 + 그 값이 바뀔 때마다
useEffect(() => {
console.log('count가 바뀜:', count);
}, [count]);
// 2) [] — 첫 렌더 후 딱 1회 (초기화용)
useEffect(() => {
console.log('컴포넌트가 처음 등장');
}, []);
// 3) 배열 생략 — 렌더될 때마다 매번
useEffect(() => {
console.log('무언가 렌더됨');
});
| 두 번째 인자 | 실행 시점 | 주 용도 |
|---|---|---|
[a, b] | 첫 렌더 후 + a나 b가 바뀔 때 | 특정 값 변화에 반응 |
[] | 첫 렌더 후 1회 | 초기 설정(구독·최초 로드) |
| (없음) | 매 렌더마다 | 거의 안 씀 |
한 가지 자주 헷갈리는 점. 의존성 배열이 있든 없든, 이펙트는 첫 렌더 직후 최소 한 번은 실행됩니다. [count]라고 적었다고 “count가 바뀔 때만” 도는 게 아니라, “처음 한 번 + 이후 count가 바뀔 때”입니다. 초기 1회 실행이 필요 없다면 이펙트 안에서 조건으로 걸러 줘야 합니다.
흔한 오해: 아무 데나 쓰지 않는다
useEffect는 강력해서 초보일수록 남용하기 쉽습니다. 하지만 다음 두 가지는 useEffect가 필요 없는 대표적인 경우입니다.
- 다른 상태로부터 계산되는 값: 예를 들어
firstName,lastName으로fullName을 만든다면, 이펙트로 상태를 또 만들 필요 없이 렌더 중에 그냥 계산하면 됩니다. - 사용자 이벤트에 대한 반응: 버튼 클릭에 반응하는 건 이펙트가 아니라
onClick같은 이벤트 핸들러의 몫입니다.
useEffect는 “렌더링만으로는 할 수 없는, 바깥 세계와의 동기화”가 필요할 때 꺼내는 도구라고 기억해 두면 남용을 피할 수 있습니다.
시작이 있으면 정리도 필요하다
useEffect로 타이머를 켜거나 서버에 연결했다면, 컴포넌트가 사라질 때 그것들을 꺼 주는 뒷정리가 필요합니다. 켜기만 하고 끄지 않으면 메모리 누수나 중복 연결 같은 문제가 생기니까요.
useEffect는 이를 위해, 설정 함수가 또 다른 함수를 반환하도록 해서 ‘정리(cleanup)’ 단계를 붙일 수 있게 해 두었습니다.
useEffect(() => {
const id = setInterval(tick, 1000); // 설정: 타이머 켜기
return () => clearInterval(id); // 정리: 타이머 끄기
}, []);
이 정리 함수가 정확히 언제, 왜 실행되는지는 처음엔 꽤 헷갈리는 부분입니다. 그래서 다음 글에서 useEffect의 정리 함수만 따로 떼어 자세히 다루겠습니다.