
😎 useEffect란
애플리케이션 내 컴포넌트의 여러 값들을 활용해 동기적으로 부수 효과를 만드는 메커니즘이다.
이 부수효과가 '언제' 일어나는지보다 어떤 상태값과 함께 실행되는지 살펴보는 것이 중요하다.
useEffect의 일반적인 형태
const CustomComponent = () => {
useEffect(() => {
}, [props, state]);
}
첫 번째 인수로는 실행할 부수효과가 포함된 함수를 전달하고 두 번째 인수로는 의존성 배열을 전달한다.
콜백함수의 실행
useEffect에서는 단순하게 의존성 배열로 전달한 값이 이전과 다른 게 하나라도 있으면 콜백함수를 실행한다.
🔧 클린업
클래스형 컴포넌트 구현에서의 언마운트와 비슷하다. 하지만 엄밀히 말하자면 언마운트 개념과는 조금 차이가 있다.
클린업 함수는 함수형 컴포넌트가 리렌더링됐을 때 의존성 변화가 있었을 당시 이전의 값을 기준으로 실행되는, 말 그대로 이전 상태를 청소해 주는 개념이다.
클린업 함수의 간단한 예시
import React from 'react';
const UseEffect = () => {
const [number, setNumber] = React.useState(0);
React.useEffect(() => {
console.log(number);
return () => {
console.log(number);
}
}, [number]);
const onClick = () => {
setNumber(number + 1);
}
return (
<div>
<button onClick={onClick}>Click</button>
</div>
);
};
export default UseEffect;
위 코드에서 number값의 변화를 감지하여 cleanup 함수를 실행한다.
cleanup 함수에서는 변화 이전의 값을 기준으로 실행된다.

위 출력에서 클린업 함수에서의 콘솔 출력은 증가하기 이전의 값 0이 출력되는 것을 확인할 수 있다.
useEffect의 서버사이드 렌더링 관점
만약 의존성 배열이 아예 없는(전달하지 않은) 경우엔 useEffect를 사용하지 않아도 되는 것이 아닌가 라는 질문을 할 수 있다.
function Component() {
console.log('렌더링 됨');
}
useEffect를 사용하면 클라이언트 사이드에서 실행되는 것을 보장해주므로 서버사이드에서 실행되지 않는다. 하지만 위 컴포넌트는 서버사이드에서 실행이 가능하다.
function Component() {
useEffect(() => {
console.log('렌더링 됨');
});
}
위 경우는 클라이언트 사이드에서 실행되는 것이 보장된다. 무거운 작업일 수록 렌더링 성능을 방해할 수 있다.
useEffect를 사용할 때 주의할 점
useEffect를 잘 못 사용하면 예기치 못한 버그가 발생할 수 있고, 심한 경우 무한 루프에 빠질 수 있다.
eslint-disable-line react-hooks/exhaustive-deps 주석은 최대한 자제해야 한다.
useEffect의 콜백 함수 내부에서 사용하는 외부 값이 의존성 배열에 포함되어 있지 않은 경우 경고를 발생시킨다.
useEffect(() => {
console.log(number);
}, [])
로깅을 위한 코드 예시
function Component({log} : {log: string}) {
useEffect(() => {
logging(log);
}, [])
}
위 코드에서 컴포넌트는 log가 아무리 변하더라도 useEfect의 부수효과(콜백 함수)는 실행되지 않고, useEffect의 흐름과 컴포넌트의 props.log의 흐름이 맞지 않게 된다.
위 경우 log를 전달하는 부모 컴포넌트에서 logging을 하는 방식으로 전환할 필요가 있는지 검토해야 한다.
useEffect의 부수효과(콜백 함수)에 함수명을 부여하자
경험상 useEffect의 동작을 파악하기 힘들어 항상 코드를 분석했었다. 교재에서 언급한 방법으로는 부수효과에 이름을 붙이는 것이다.
useEffect(function fetchData() {
...
}, [...]);
이 로서 useEffect의 목적을 명확히 하고 그 책임을 최소한으로 좁히는 데 고민할 수 있을 것이다.
거대한 useEffect를 만들지 말자
useEffect는 렌더링 성능에 기여하는 정도가 크다. 따라서 useEffect의 사이즈가 크다면 여러 useEffect로 분리하고 메모이제이션 도입을 검토해보자.
불필요한 외부 함수를 만들지 말자
경험상 API요청 로직을 따로 분리하여 초기 렌더링시 fetch하는 경우를 구현하였다.
function Component({id} : {id: string}) {
const fetchData = useCallback(async () => {
const res = await fetch(...);
return res.data;
}, []);
useEffect(() => {
fetchData(id);
});
}
useEffect가 사용하는 로직이 불필요하게 존재하면 가독성이 떨어지고 실제 컴포넌트의 로직을 한눈에 파악하기 힘들다.
이를 해결하기 위해 부수효과를 비동기 함수로 수정하면 오류가 발생한다.
틀린 구현이다.
function Component({id} : {id: string}) {
useEffect(async () => {
const res = await fetch(...);
setData(res.data);
});
}
렌더링이 여러 번 발생한 경우 이전의 응답이 10초가 걸리고 그 다음번째 응답이 1초내에 왔다면 경쟁상태가 발생할 수 있다. 이는 이전 결과로 바뀌는 오류가 발생할 수 있다.
즉시 실행 함수를 통해 비동기 요청을 수행하자
즉시 실행 함수를 이용하면 비동기 요청을 useEffect내부에서 수행할 수 있다.
function Component({id} : {id: string}) {
useEffect(() => {
;(async () => {
const result = await fetch(...);
setInfo(await result.json());
})()
});
}
로직의 의미를 나타내기 위해 이름을 붙이자
function Component({id} : {id: string}) {
useEffect(function fetchArticleData() => {
;(async function fetchData() {
const result = await fetch(...);
setInfo(await result.json());
})();
}, []);
}
🧐 useEffect를 사용하는 커스텀 hooks
API 요청 후 응답을 받아 렌더링하는 컴포넌트가 있다고 할 때, status, result, ok... 등등 여러 상태를 생성해야 한다.
대신에 커스텀 훅을 만들어 로직을 단순화 할 수 있다. 커스텀 훅을 구현한 파일의 이름은 use로 시작해야 한다.
useFetch.tsx
import React from 'react';
function useFetch<T>(
url: string,
{ method, body }: { method: string; body?: XMLHttpRequestBodyInit },
) {
const [result, setResult] = React.useState<T | undefined>();
const [isLoading, setIsLoading] = React.useState(false);
const [ok, setOk] = React.useState<boolean | undefined>();
const [status, setStatus] = React.useState<number | undefined>();
React.useEffect(() => {
const abortController = new AbortController();
;(async () => {
setIsLoading(true);
const response = await fetch(url, {
method,
body,
signal: abortController.signal,
})
setOk(response.ok);
setStatus(response.status);
if(response.ok) {
const apiResult = await response.json();
setResult(apiResult);
}
setIsLoading(false);
})();
}, [url, method, body]);
return { result, isLoading, ok, status }
}
export default useFetch;
App.tsx
import React, { useEffect } from 'react';
import './App.css';
import useFetch from './Chapter3/useFetch';
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
function App() {
const { result, isLoading, ok, status } = useFetch<Array<Todo>>(
'https://jsonplaceholder.typicode.com/todos',
{ method: 'GET' }
);
useEffect(() => {
if(!isLoading) {
console.log(`fetchResult >> ${status}`);
}
}, [status, isLoading]);
return (
<div>
{
ok ? (result || []).map((todo: Todo) => (
<div key={todo.id}>
<h1>{todo.title}</h1>
<p>{todo.completed ? 'Completed' : 'Not Completed'}</p>
</div>
)) : null
}
</div>
);
}
export default App;
abortController에 대해
abortController는 비동기 요청을 취소할 수 있게 해준다.
...
React.useEffect(() => {
const abortController = new AbortController();
;(async () => {
setIsLoading(true);
const response = await fetch(url, {
method,
body,
signal: abortController.signal,
})
...
컴포넌트가 요청 중에 언마운트 되거나 재 실행되었을 때 불필요한 요청을 취소하기 위해 사용한다.
abortController객체를 생성하여 fetch함수의 signal에 전달해준다.
'React' 카테고리의 다른 글
| React Native / Prettier설정 (0) | 2025.02.23 |
|---|---|
| React / React + Typescript / useMemo에 대하여... (0) | 2025.02.05 |
| React / React + Typescript / 초기 프로젝트 세팅 (0) | 2025.02.03 |
| React / React + Typescript / Context API를 이용한 간단한 TodoList (2) | 2025.02.01 |
| 1. 리액트 라우터(React router) (0) | 2022.12.18 |