이미지 출처: copycat.dev
useEffect
는 React에서 가장 중요한 훅 중 하나로, 함수형 컴포넌트에서 사이드 이펙트(side effects)를 처리할 수 있게 해주는 도구이다. 사이드 이펙트란 컴포넌트의 렌더링과 직접적으로 관련되지 않은 작업을 말하며, 서버에서 데이터를 가져오거나, DOM을 직접 수정하거나, 구독(subscription) 설정과 같은 작업들이 이에 해당한다. useEffect
를 통해 컴포넌트가 렌더링될 때마다 특정 작업을 수행하도록 할 수 있어, 컴포넌트 생명주기와 관련된 작업을 효과적으로 관리할 수 있다.
useEffect
를 사용해야 하는가?useEffect
는 React의 함수형 컴포넌트에서 사이드 이펙트를 처리하기 위해 설계되었다. 기존의 클래스형 컴포넌트에서는 컴포넌트의 생명주기 메서드를 통해 특정 시점에서 코드를 실행할 수 있었다. 예를 들어, 컴포넌트가 마운트되거나 업데이트될 때 componentDidMount
, componentDidUpdate
같은 메서드를 사용했다. 하지만 함수형 컴포넌트는 이러한 생명주기 메서드가 없기 때문에, 이와 유사한 기능을 수행할 수 있는 메커니즘이 필요했다. 이 문제를 해결하기 위해 React 팀은 useEffect
를 도입했다.
함수형 컴포넌트는 클래스형 컴포넌트에 비해 코드가 단순하고 가독성이 높다는 장점이 있다. 그러나 상태 관리와 사이드 이펙트 관리를 위해 생명주기 메서드를 직접 구현해야 한다면, 이러한 단순성이 사라질 수 있다. useEffect
는 이러한 문제를 해결하면서도 함수형 컴포넌트의 단순성을 유지할 수 있게 해준다.
useEffect
는 데이터 가져오기, DOM 업데이트, 구독 설정 등 다양한 사이드 이펙트를 하나의 훅에서 관리할 수 있게 해준다. 이는 클래스형 컴포넌트에서 각 작업을 다른 생명주기 메서드에서 처리해야 했던 것과 비교해 훨씬 효율적이다.
클래스형 컴포넌트에서는 동일한 작업을 여러 생명주기 메서드에서 처리해야 할 경우가 있었다. 예를 들어, 컴포넌트가 마운트될 때와 업데이트될 때 동일한 API 호출이 필요하다면, componentDidMount
와 componentDidUpdate
에서 동일한 코드를 작성해야 했다. useEffect
를 사용하면 이 모든 작업을 하나의 함수 안에 통합할 수 있어 코드 중복을 줄일 수 있다.
useEffect
는 사이드 이펙트로 발생한 리소스들을 쉽게 정리(cleanup)할 수 있게 해준다. 예를 들어, 타이머를 설정하거나 외부 데이터 소스에 구독을 설정하는 경우, 컴포넌트가 언마운트될 때 해당 리소스를 해제해야 한다. useEffect
는 이를 단순하게 처리할 수 있는 방법을 제공한다. 이는 컴포넌트의 메모리 누수나 불필요한 리소스 사용을 방지하는 데 도움이 된다.
useEffect
의 기본 사용법useEffect
는 다음과 같은 형식으로 사용된다.
import React, { useEffect } from 'react';
function ExampleComponent() {
useEffect(() => {
// 여기서 실행할 코드를 작성한다.
});
return <div>Example</div>;
}
이 코드는 컴포넌트가 처음 렌더링된 후에 실행된다. 기본적으로 useEffect
는 매 렌더링 이후에 실행되지만, 의존성 배열을 통해 실행 시점을 제어할 수 있다.
useEffect
에서의 의존성 배열useEffect
는 React 컴포넌트가 렌더링된 후에 특정 작업을 수행하기 위해 사용하는 훅이다. 이때 의존성 배열을 통해 특정 상태나 props의 변경에 따라 useEffect
를 재실행할지를 결정할 수 있다.
useEffect(() => {
// 어떤 작업 수행
}, [dependency1, dependency2]);
여기서 dependency1
과 dependency2
는 의존성 배열이다. 이 배열에 포함된 값들 중 하나라도 변경되면, useEffect
내의 콜백 함수가 다시 실행된다.
[]
): 컴포넌트가 처음 마운트될 때만 useEffect
가 실행되고, 이후에는 재실행되지 않는다. 이를 통해 초기화 작업 등을 수행할 수 있다.undefined
): 배열을 생략하면, 컴포넌트가 렌더링될 때마다 useEffect
가 실행된다.[stateA, propB]
): 배열에 포함된 값들이 변경될 때만 useEffect
가 실행된다. 이로 인해 필요한 경우에만 부수 효과(Side Effect)가 발생하도록 제어할 수 있다.useEffect
의 정리(clean-up) 함수useEffect
안에서 반환되는 함수는 컴포넌트가 언마운트될 때 혹은 다음 렌더링 전에 실행된다. 이를 통해 타이머나 구독(subscription) 같은 리소스를 정리할 수 있다.
useEffect(() => {
const timer = setTimeout(() => {
console.log('타이머 실행');
}, 1000);
return () => {
clearTimeout(timer); // 컴포넌트가 언마운트되기 전에 타이머를 정리
};
}, []);
위 예제에서 clearTimeout(timer)
은 컴포넌트가 언마운트될 때 호출되어 타이머를 정리한다. 이처럼 useEffect
는 리소스를 효율적으로 관리하는 데 중요한 역할을 한다.
useEffect
의 동작 원리useEffect
는 React의 렌더링 단계가 모두 끝난 후에 실행된다. React는 컴포넌트를 렌더링할 때, 먼저 모든 컴포넌트의 렌더링을 완료한 후, 각 컴포넌트에 설정된 useEffect
를 실행한다. 이 때문에 useEffect
는 DOM에 접근하거나, 서버에서 데이터를 가져오는 작업에 적합하다. 이는 브라우저가 실제로 화면을 업데이트한 후에 이펙트가 실행되기 때문에, 렌더링 과정에 영향을 주지 않기 때문이다.
때로는 useEffect
가 수행한 작업을 정리(cleanup)해야 할 필요가 있다. 예를 들어, 구독을 설정한 경우 컴포넌트가 언마운트될 때 구독을 해제해야 한다. 이를 위해 useEffect
는 클린업 함수를 반환할 수 있다. 이 함수는 컴포넌트가 언마운트되거나 이펙트가 다시 실행되기 전에 호출된다.
useEffect(() => {
const subscription = someAPI.subscribe();
return () => {
// cleanup 함수
subscription.unsubscribe();
};
}, []);
이 코드에서 구독은 컴포넌트가 마운트될 때 설정되고, 컴포넌트가 언마운트될 때 해제된다.
useEffect
의 실행 시점useEffect
는 컴포넌트가 처음 화면에 렌더링될 때 실행된다. 이때 초기 데이터 로드, 구독 설정 등의 작업을 수행할 수 있다.
useEffect
는 의존성 배열에 포함된 값이 변경될 때마다 실행된다. 이를 통해 특정 값이 변경될 때만 특정 작업을 수행하도록 제어할 수 있다. 예를 들어, 사용자가 입력한 값이 변경될 때마다 서버로 API 요청을 보내고 싶다면, 해당 값을 의존성 배열에 추가하면 된다.
useEffect
는 컴포넌트가 언마운트되기 직전에 정리(cleanup) 함수를 호출한다. 이때 구독 해제, 타이머 제거 등의 작업을 수행할 수 있다. 이를 통해 메모리 누수나 불필요한 작업을 방지할 수 있다.
useEffect
가 실행되는 이유리액트는 컴포넌트가 렌더링된 이후에 useEffect
를 실행하는데, 이에는 중요한 이유가 있다. 만약 렌더링 전에 useEffect
가 실행된다면, 가상 돔이 Side Effect와 함께 처리되기 때문에, 성능 저하가 발생할 수 있다.
예를 들어, 렌더링 과정에서 state
가 변경되면 추가적인 렌더링이 필요하게 된다. 따라서, useEffect
를 렌더링 이전에 실행하면 한 번에 처리할 수 있는 작업을 두 번의 렌더링 과정으로 나누게 되어 비효율적이다.
특히, Side Effect가 포함된 렌더링은 가상 돔을 자주 다시 렌더링하게 만들 수 있다. 게다가, 일부 Side Effect는 서버의 응답을 받아야만 다음 작업을 진행할 수 있는 경우도 있는데, 이런 경우 렌더링 과정이 지연될 수 있다.
이 때문에 리액트는 가상 돔 렌더링에서 Side Effect를 제외하고, 순수한 컴포넌트만을 사용해 렌더링을 진행한다. 이렇게 해야만 리액트의 핵심인 재조정 알고리즘이 효율적으로 작동할 수 있다.
또한, 렌더링 이후에 비동기 Side Effect를 처리하는 것은 사용자 경험에도 긍정적인 영향을 준다. 만약 서버와의 통신에 문제가 발생해도, 이로 인한 영향을 최소화할 수 있기 때문이다. 그래서 시간이 많이 걸리거나 비동기 작업을 처리할 때 useEffect
가 특히 유용한 것이다.
useEffect
와 비동기 작업useEffect
내에서 비동기 작업을 수행할 때는 주의가 필요하다. 비동기 작업을 직접 useEffect
함수 내에서 수행하지 말고, 비동기 함수를 따로 정의한 후 호출하는 것이 좋다. 이는 비동기 작업이 완료되기 전에 컴포넌트가 언마운트되거나, 다른 비동기 작업이 시작될 수 있기 때문이다.
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
이 예제에서 fetchData
함수는 컴포넌트가 마운트될 때 한 번 호출되며, 데이터를 가져온 후 상태를 업데이트한다.
useEffect
의 잘못된 사용 예만약 useEffect
내에서 상태를 변경하고, 그 상태가 의존성 배열에 포함되어 있으면, 이펙트가 무한히 반복해서 실행될 수 있다.
useEffect(() => {
setCount(count + 1); // 잘못된 예시
}, [count]);
이 코드는 count
가 변경될 때마다 useEffect
가 실행되고, 다시 count
를 변경하므로 무한 루프에 빠지게 된다. 이를 방지하려면 상태 변경과 관련된 작업은 조건부로 실행하거나, 적절한 의존성을 설정해야 한다.
의존성 배열을 생략하면, useEffect
는 모든 렌더링 후에 실행된다. 이는 의도하지 않은 사이드 이펙트를 초래할 수 있다.
useEffect(() => {
fetchData(); // fetchData가 의존성 배열에 포함되어야 한다
});
이 코드는 매번 렌더링될 때마다 데이터를 가져오므로 성능 문제를 일으킬 수 있다. 따라서 의존성 배열을 정확히 설정하여 이펙트의 실행 빈도를 제어해야 한다.
useEffect
의 활용과 유용한 패턴가장 일반적인 useEffect
사용 사례 중 하나는 컴포넌트가 마운트될 때 데이터를 가져오는 것이다.
useEffect(() => {
async function fetchData() {
const response = await fetch('/api/data');
const data = await response.json();
setData(data);
}
fetchData();
}, []);
이 코드에서 useEffect
는 컴포넌트가 처음 렌더링될 때 한 번 실행되며, 데이터를 가져오고 상태를 업데이트한다.
구독이나 타이머 설정 등, 클린업이 필요한 작업도 useEffect
로 관리할 수 있다.
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id); // 컴포넌트 언마운트 시 타이머 해제
}, []);
이 코드는 1초마다 count
를 증가시키고, 컴포넌트가 언마운트되면 타이머를 해제한다.
useEffect
를 사용할 때 고려해야 할 사항useEffect
와 상태 업데이트: 상태를 업데이트하는 로직은 신중하게 설계해야 한다. 무한 루프에 빠지지 않도록 주의하고, 필요할 때만 상태를 업데이트해야 한다.