사이드 이펙트와의 첫 만남 - useEffect 기본기
리액트 공식문서 기반의 리액트 입문기
들어가며
지금까지 우리는 리액트를 사용하여 선언적인 방식으로 UI를 구축하고, useState 훅을 통해 컴포넌트 내부의 상태를 관리하는 방법을 살펴보았습니다. 하지만 애플리케이션 개발에서는 UI 렌더링이나 상태 변경 외에 외부 시스템과의 상호작용이 필요한 경우가 자주 발생합니다.
예를 들어, 데이터를 서버에서 가져오거나, 구독을 설정하거나, DOM을 직접 조작하는 등의 작업이 이에 해당합니다. 이러한 작업들을 리액트에서는 '사이드 이펙트(Side Effect)'라고 부르며, 이를 처리하기 위해 useEffect 훅을 제공합니다.
이번 편에서는 useEffect 훅이 무엇인지, 왜 필요한지, 그리고 어떻게 기본적인 사용법을 익힐 수 있는지 저의 학습 여정을 공유하고자 합니다.
useEffect 기본기 - 동기화의 시작
리액트 컴포넌트는 기본적으로 순수 함수처럼 동작하여, 동일한 props와 state가 주어지면 항상 동일한 UI를 렌더링해야 합니다. 하지만 앞서 언급한 사이드 이펙트는 이러한 순수성을 깨뜨릴 수 있습니다.
useEffect는 이러한 사이드 이펙트를 컴포넌트의 렌더링 로직과 분리하여, 리액트의 예측 가능한 동작 방식을 유지하면서도 외부 시스템과 안전하게 동기화할 수 있도록 돕습니다. 마치 리액트의 울타리 밖에서 외부 세계와 소통하는 통로를 만들어주는 것과 같다는 생각을 합니다.
useEffect 훅은 컴포넌트가 렌더링된 이후에 특정 작업을 수행해야 할 때 사용됩니다. 데이터 페칭, 구독 설정, 수동적인 DOM 조작 등이 대표적인 예시입니다. useEffect는 두 개의 주요 인자를 받습니다.
첫 번째는 수행할 '이펙트 함수(Effect Function)'이고, 두 번째는 이 이펙트가 다시 실행될지 여부를 결정하는 '의존성 배열(Dependency Array)'입니다. 의존성 배열을 생략하거나 빈 배열로 두는 경우 등 다양한 실행 타이밍이 있으며, 이는 5-6편에서 더 자세히 다루려고 합니다.
useEffect로 사이드 이펙트 관리하기
useEffect를 사용하면 컴포넌트의 생명주기(마운트, 업데이트)에 맞춰 사이드 이펙트를 실행할 수 있습니다. 예를 들어, 브라우저의 전역 객체인 window에 이벤트 리스너를 등록하거나, 컴포넌트의 상태 변화에 따라 문서의 제목을 업데이트하는 등의 작업이 이에 해당합니다. 이러한 과정은 컴포넌트가 처음 마운트될 때 실행되고, 특정 조건에 따라 다시 실행될 수 있습니다.
1) 스크롤 위치 로거 예제
useEffect는 브라우저의 전역 객체인 window와 같은 외부 시스템과 컴포넌트를 동기화하는 데 유용하게 사용될 수 있습니다. 다음은 컴포넌트가 마운트될 때 스크롤 이벤트 리스너를 등록하고, 스크롤 위치에 따라 상태를 업데이트하며, 컴포넌트가 언마운트될 때 이벤트 리스너를 해제하는 예제입니다. 이는 useEffect의 사이드 이펙트 관리 및 클린업 메커니즘을 명확하게 보여주는 사례입니다.
import { useState, useEffect } from 'react';
function ScrollPositionLogger() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
};
window.addEventListener('scroll', handleScroll);
console.log('스크롤 이벤트 리스너 등록됨');
// 클린업 함수: 컴포넌트 언마운트 시 이벤트 리스너 해제
return () => {
window.removeEventListener('scroll', handleScroll);
console.log('스크롤 이벤트 리스너 해제됨');
};
}, []); // 빈 의존성 배열: 마운트 시 한 번만 등록/해제
return (
<div style={{ height: '2000px' }}>
<h2>스크롤 위치 로거</h2>
<p>현재 스크롤 위치: {scrollY}px</p>
<p>(페이지를 스크롤하여 변화를 확인해 보세요)</p>
</div>
);
}
export default ScrollPositionLogger;ScrollPositionLogger 컴포넌트에서는 useEffect를 사용하여 window 객체에 스크롤 이벤트 리스너를 등록합니다. 스크롤이 발생할 때마다 setScrollY를 통해 scrollY 상태를 업데이트하며, 이는 현재 스크롤 위치를 UI에 표시합니다. 의존성 배열이 []로 비어 있기 때문에, 이 이펙트 함수는 컴포넌트가 처음 마운트될 때 한 번만 실행됩니다. 여기서 return 문 안에 있는 함수는 컴포넌트가 언마운트될 때 실행되어 이벤트 리스너를 해제하는 '클린업(cleanup)' 함수입니다. 클린업은 불필요한 이벤트 리스너의 누적을 방지하여 메모리 누수를 막는 데 중요한 역할을 합니다. 이에 대해서는 5-6편: Effect의 생명주기 관리 - 의존성 배열과 클린업 함수에서 더 자세히 다루겠습니다.
2) 문서 제목 동기화 예제
useEffect는 컴포넌트의 상태 변화에 따라 브라우저의 document.title과 같은 외부 시스템을 업데이트하는 데도 유용하게 사용될 수 있습니다. 다음은 카운트 상태에 따라 웹 페이지의 제목을 변경하는 예제입니다.
import { useState, useEffect } from 'react';
function TitleUpdater() {
const [count, setCount] = useState(0);
useEffect(() => {
// Effect 함수: document.title 업데이트
document.title = `Count: ${count}`;
}, [count]); // count가 변경될 때마다 이펙트가 다시 실행됩니다.
return (
<div>
<h2>문서 제목 동기화 예제</h2>
<p>현재 카운트: {count}</p>
<button onClick={() => setCount(prevCount => prevCount + 1)}>카운트 증가</button>
<p>(브라우저 탭/창 제목을 확인해 보세요.)</p>
</div>
);
}
export default TitleUpdater;TitleUpdater 컴포넌트에서는 count 상태가 변경될 때마다 document.title을 업데이트하는 useEffect가 사용됩니다. 의존성 배열에 [count]를 포함하여, count 값이 변경될 때마다 이펙트 함수가 다시 실행되어 브라우저 탭의 제목이 현재 카운트를 반영하도록 동기화됩니다. 이는 useEffect가 React 내부 상태와 브라우저 DOM이라는 외부 시스템을 연결하는 강력한 도구임을 보여주는 또 다른 예시라고 생각합니다.
useEffect와 useRef의 시너지: 컴포넌트 생애주기 내 DOM 조작
이전 5-1편에서 useRef가 리렌더링 없이 값을 저장하는 용도로 사용될 수 있음을, 그리고 5-2편에서는 ref를 통해 DOM을 직접 조작하는 '탈출구' 역할을 살펴보았습니다. ref를 통한 DOM 조작은 리액트의 제어 흐름을 벗어나는 명령형 작업이므로, 컴포넌트의 생애주기에 맞춰 안전하게 실행하는 것이 중요합니다. 이때 useEffect와 useRef를 함께 사용하면 이러한 명령형 DOM 조작을 리액트의 생명주기 안에서 적절한 시점에 수행할 수 있습니다.
1) useRef를 통한 입력 필드 자동 포커스 예제
5-2편에서 보았던 입력 필드에 자동으로 포커스를 주는 예제를 useEffect와 함께 사용하면, 컴포넌트가 처음 마운트될 때 한 번만 포커스를 적용하는 로직을 구현할 수 있습니다.
import { useRef, useEffect } from 'react';
function FocusInputOnMount() {
const inputRef = useRef(null);
useEffect(() => {
// 컴포넌트가 마운트될 때 한 번만 실행됩니다.
if (inputRef.current) {
inputRef.current.focus();
}
}, []); // 빈 의존성 배열: 마운트 시 한 번만 실행
return (
<div>
<h2>useRef와 useEffect로 자동 포커스</h2>
<input type="text" ref={inputRef} placeholder="여기에 자동으로 포커스" />
</div>
);
}
export default FocusInputOnMount;FocusInputOnMount 컴포넌트에서는 inputRef로 input 요소에 대한 참조를 얻고, useEffect 훅 내부에서 inputRef.current.focus()를 호출합니다. 의존성 배열이 비어있으므로 이 이펙트는 컴포넌트가 처음 마운트될 때 단 한 번만 실행되며, 이는 useEffect가 useRef를 활용하여 외부 시스템(브라우저 DOM)과 컴포넌트 생애주기를 동기화하는 명확한 사례가 됩니다.
2) useRef를 활용한 타이머 ID 저장 (간략한 언급)
5-1편에서 useRef가 타이머 ID와 같은 리렌더링과 무관한 값을 저장하는 데 유용하다고 언급했습니다. useEffect 내에서 setInterval과 같은 타이머를 시작하고 그 ID를 useRef에 저장하면, 나중에 이 ID를 사용하여 타이머를 제어할 수 있습니다. 이렇게 useEffect는 타이머 시작이라는 사이드 이펙트를 수행하고, useRef는 그 타이머의 ID를 안전하게 보존하는 역할을 분담할 수 있습니다. (타이머 정리와 같은 클린업 로직은 5-6편에서 더 자세히 다룰 예정입니다.)
useEffect의 특징
- 외부 시스템과의 동기화:
useEffect는 리액트 컴포넌트가 렌더링된 후, 브라우저 DOM이나 네트워크, 서드파티 라이브러리 등과 같은 외부 시스템과 상태를 동기화하는 데 사용됩니다. 이는 React의 순수성을 유지하면서 외부 세계와 상호작용할 수 있도록 돕는 중요한 통로 역할을 합니다. - 컴포넌트 생명주기와의 연결:
useEffect는 컴포넌트가 화면에 처음 나타날 때(마운트), 업데이트될 때, 그리고 화면에서 사라질 때(언마운트)와 같은 생애주기 시점에 맞춰 특정 작업을 실행하거나 정리할 수 있도록 합니다. 이는 마치 컴포넌트의 각 '삶의 단계'에 필요한 부수적인 행동들을 연결해주는 것과 같습니다.useEffect의 생명주기 및 클린업 메커니즘에 대한 더 자세한 이해는5-6편: Effect의 생명주기 관리 - 의존성 배열과 클린업 함수에서 심층적으로 다룰 예정입니다. - 렌더링 로직으로부터의 분리:
useEffect내부의 코드는 컴포넌트의 주요 렌더링 로직(UI를 계산하고 반환하는 부분)과는 독립적으로 실행됩니다. 이 덕분에 UI 계산이 더욱 예측 가능하고 효율적으로 이루어질 수 있으며, 복잡한 외부 상호작용 로직이 UI 코드와 섞이지 않아 코드의 가독성이 높아진다는 장점이 있습니다. - 의존성 배열을 통한 실행 제어:
useEffect훅은 두 번째 인자로 받는 '의존성 배열'을 통해 이펙트 함수가 언제 다시 실행될지 제어할 수 있습니다. 배열에 포함된 값이 변경될 때만 이펙트 함수가 재실행되므로, 불필요한 반복 실행을 방지하고 성능을 최적화하는 데 중요한 역할을 합니다.5-6편에서는 이 의존성 배열의 심층적인 동작 원리와 생명주기 관점에서의 활용을 더욱 자세히 다룰 예정입니다. 이 배열의 사용 여부와 내용에 따라useEffect의 동작 방식을 매우 유연하게 조절할 수 있다는 점이 특징입니다. - 신중한 사용 권장:
useEffect는 외부 시스템과의 동기화를 위한 강력한 도구이지만, 무분별하게 사용하면 오히려 코드를 복잡하게 만들거나 예상치 못한 버그를 유발할 수 있습니다. 모든 로직이useEffect안에 들어가야 하는 것은 아니며, 때로는useEffect없이도 더 간단하고 효율적으로 문제를 해결할 수 있는 방법들이 있다는 것을 항상 염두에 두어야 합니다.
요약
이전 섹션에서 우리는 useEffect 훅의 기본 개념과 사이드 이펙트를 관리하는 중요성을 살펴보았습니다. useEffect가 리액트 컴포넌트의 순수성을 유지하면서 외부 시스템과의 상호작용을 안전하게 처리할 수 있도록 돕는다는 것을 알 수 있었습니다. 특히 useRef와 함께 사용될 때, 컴포넌트 생애주기에 맞춰 명령형 DOM 조작을 안전하게 수행하는 시너지를 발휘한다는 점도 확인할 수 있었습니다.
하지만 useEffect가 강력한 도구인 만큼, 모든 상황에 필요한 것은 아닙니다. useEffect 없이도 해결할 수 있는 문제들이 있으며, 이에 대해서는 다음 편인 5-4편에서 더 자세히 다룰 예정입니다. 다음은 이번 편의 핵심 내용들입니다.
useEffect의 역할: 리액트 컴포넌트 내에서 외부 시스템과의 동기화 및 사이드 이펙트(스크롤 이벤트 리스너 등록/해제, DOM 조작,document.title업데이트 등)를 관리하는 훅입니다.- 기본 사용법:
useEffect(setup, dependencies)형태로 사용하며,setup함수는 이펙트 로직을 포함하고,dependencies배열은 이펙트 재실행 조건을 제어합니다. 의존성 배열의 심층적인 내용은5-6편에서 다룹니다. useRef와의 연계:useRef로 얻은 DOM 참조를useEffect내에서 사용하여 컴포넌트 생애주기에 맞춰 명령형 DOM 조작을 안전하게 수행할 수 있습니다.- 동기화 시점: 컴포넌트가 렌더링된 이후에 실행되어, UI가 화면에 반영된 후에 필요한 부수 작업을 처리할 수 있습니다.
- 핵심 특징: 외부 시스템과의 동기화, 컴포넌트 생애주기와의 연결, 렌더링 로직으로부터의 분리, 의존성 배열을 통한 실행 제어, 신중한 사용 권장.
이번 편을 통해 useEffect의 기본적인 동작 원리와 useRef와의 시너지를 이해하는 데 도움이 되었기를 바랍니다.