useEffect
useEffect란?
useEffect는 리액트의 함수형 컴포넌트에서 사이드 이펙트(Side Effects)를 처리하기 위해 사용하는 훅입니다.
SideEffect란?
사이드 이펙트란 컴포넌트가 렌더링될 때 DOM을 직접 조작하거나, 데이터를 가져오거나, 타이머를 설정하는 등 컴포넌트의 렌더링과는 별개로 외부와 상호작용하는 작업을 말합니다.
기본적으로 useEffect는 컴포넌트가 렌더링된 후 실행되며, 특정 상태나 props가 변경될 때마다 다시 실행할 수 있습니다.
componentDidMount(처음 마운트 시 실행)componentDidUpdate(업데이트 시 실행)componentWillUnmount(언마운트 시 실행)
useEffect(() => {
// mount 시점, deps update 시점에 실행할 작업 (componentDidMount)
return () => {
//unmount 시점, deps update 직전에 실행할 작업 (componentWillUnmount)
};
}, [dep1, dep2]);
useEffect에 넘기는 함수(콜백 함수)
- return으로 넘기는 함수(클린업 함수)
- dependency 배열
→ componentDidMount componentDidUpdate componentWillUnmount 의 생명 주기 매서드를 대체한다.
의존성 배열의 다양한 사용 방법
1. 빈 배열 [] (처음 마운트될 때만 실행):
- 의존성 배열이 비어 있을 경우, useEffect는 한 번만 실행됩니다. 이는 컴포넌트가 처음 마운트될 때 실행되고, 이후에는 실행되지 않습니다.
useEffect(() => {
console.log('컴포넌트가 마운트될 때 한 번 실행');
}, []); // 빈 배열
-
이 방식은 주로 API 호출, 타이머 설정 등 초기 작업을 처리할 때 사용됩니다.
2. 특정 값이 있을 때 실행 (값이 변경될 때마다 실행):
-
의존성 배열에 상태(state)나 props를 넣으면, 해당 값이 변경될 때마다 useEffect가 다시 실행됩니다. 이를 통해 특정 값이 변할 때마다 효과를 트리거할 수 있습니다.
useEffect(() => {
console.log(`count가 ${count}로 변경되었습니다.`);
}, [count]); // count가 변경될 때마다 실행
-
이 방식은 상태나 props의 변화에 맞춰 데이터를 다시 가져오거나 UI를 업데이트할 때 유용합니다.
3. 의존성 배열을 생략할 때 (매 렌더링마다 실행):
-
의존성 배열을 생략하면 useEffect는 컴포넌트가 리렌더링될 때마다 실행됩니다. 즉, 모든 상태 변화나 props 변경에 반응합니다.
useEffect(() => {
console.log('컴포넌트가 매 렌더링마다 실행됩니다.');
}); // 의존성 배열 없음
-
이 방식은 일반적으로 비효율적이므로, 필요한 경우에만 사용해야 합니다.
4. 정리 작업 (clean-up):
-
useEffect에서 정리 작업을 정의하면, 컴포넌트가 언마운트될 때 또는 의존성 배열에 있는 값이 변경될 때 정리 작업이 실행됩니다. 이를 통해 메모리 누수나 불필요한 자원 낭비를 방지할 수 있습니다.
useEffect(() => {
const timer = setInterval(() => {
console.log('타이머 실행 중...');
}, 1000);
return () => {
clearInterval(timer); // 타이머 정리console.log('타이머 정리됨');
};
}, []); // 컴포넌트가 마운트될 때 한 번 실행
1️⃣ useEffect와 메모리
useEffect는 매 렌더링마다 새로 만들어진 “함수와 변수들의 스냅샷”을 메모리에 계속 쌓았다가, 실행 시점에 참조해요.
- React 컴포넌트는 함수니까, 렌더링이 일어날 때마다 함수 전체가 새로 호출돼요.
- 그럼 안에서 정의된 변수나 함수들은 그 렌더의 클로저에 캡쳐된 값을 가짐.
- useEffect는 그 렌더 당시의 클로저를 기억하고 있다가, 렌더 후에 실행돼요.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('Effect 내부 count:', count);
});
return <button onClick={() => setCount(c => c + 1)}>+</button>;
}
useEffect의 클린업 함수
클린업 함수는 주로 이벤트 리스너 등록/해제, 타이머 설정/해제, 구독 설정/해제, 리소스 정리와 같이 컴포넌트의 생명주기 동안 발생한 부작용을 정리하는 데 사용하여 메모리 누수나 불필요한 리소스 사용을 방지할 수 있다.
2️⃣ useEffect의 클로저
클로저는 함수가 선언될 때의 환경을 '기억'하며, useEffect는 컴포넌트의 생명주기에 따른 부수 효과를 관리합니다.
useEffect(() => {
fetch('https://api.example.com/data')
.then(response => response.json())
.then(data => setData(data));
}, []); // 빈 배열을 전달하여 마운트될 때만 실행
3️⃣ useEffect의 의존성 배열
다음 코드 중에서 어떤 코드에서 어떤 문제가 생길까요?
코드 1
function App() {
const [restaurantList, setRestaurantList] = useState([]);
const fetchRestaurants = async () => {
const response = await fetch(API_URL);
const data = await response.json();
setRestaurantList(data);
};
useEffect(() => {
fetchRestaurants();
}, [restaurantList]);
코드 2
function App() {
const [restaurantList, setRestaurantList] = useState([]);
const fetchRestaurants = async () => {
const response = await fetch(API_URL);
const data = await response.json();
setRestaurantList(data);
};
useEffect(() => {
fetchRestaurants();
}, [fetchRestaurants]);
코드 3
function App() {
const [restaurantList, setRestaurantList] = useState([]);
const fetchRestaurants = async () => {
const response = await fetch(API_URL);
const data = await response.json();
setRestaurantList(data);
};
useEffect(() => {
fetchRestaurants();
}, []);
- 어떤 문제가 생길까요?
- 코드 1 문제: 무한 루프
- 코드 2 문제: 역시 무한 루프 (함수 참조가 매 렌더마다 새로 생성되기 때문)
- 코드 3 문제: 참조 문제(stale closure 가능성)
👀 의존성 배열은 어떻게 값의 변경을 감지 할까?
import is from 'shared/objectIs'; // 아래 설명 참조
function areHookInputsEqual(
nextDeps: Array<mixed>, // 다음 의존성 배열
prevDeps: Array<mixed> | null // 이전 의존성 배열 (null일 수 있음)
): boolean {
// 이전 의존성 배열이 null인 경우 에러 메시지를 출력하고 false를 반환합니다.
if (prevDeps === null) {
return false;
}
// 이전 의존성 배열의 길이와 다음 의존성 배열의 길이를 비교하며 반복합니다.
for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// 현재 인덱스의 두 의존성을 사용자 정의 비교 함수로 비교합니다.
if (is(nextDeps[i], prevDeps[i])) {
continue; // 두 값이 같으면 다음 인덱스로 넘어갑니다.
}
return false; // 하나라도 다르면 false를 반환합니다.
}
return true; // 모든 요소가 같으면 true를 반환합니다.
-
렌더가 끝나면, React는 “이번 렌더의 deps(
nextDeps)”와 “이전 렌더의 deps(prevDeps)”를 비교.// 두 값을 비교하는 함수
function is(x: any, y: any) {
// - 첫 번째 조건: x와 y가 동일하고, x가 0이 아닐 경우 또는 x와 y가 모두 0인 경우
// - 두 번째 조건: x와 y가 NaN인 경우 (NaN은 자신과 같지 않음)
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}
// Object.is가 함수로 정의되어 있다면 이를 사용하고, 그렇지 않으면 사용자 정의 is 함수를 사용
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === 'function' ? Object.is : is;
export default objectIs; -
areHookInputsEqual(nextDeps, prevDeps)를 호출해서 완전히 같으면 → effect 실행 안 함 -
다르면 → cleanup → 새 effect 실행
// 두 값을 비교하는 함수
function is(x: any, y: any) {
// - 첫 번째 조건: x와 y가 동일하고, x가 0이 아닐 경우 또는 x와 y가 모두 0인 경우
// - 두 번째 조건: x와 y가 NaN인 경우 (NaN은 자신과 같지 않음)
return (x === y && (x !== 0 || 1 / x === 1 / y)) || (x !== x && y !== y);
}
// Object.is가 함수로 정의되어 있다면 이를 사용하고, 그렇지 않으면 사용자 정의 is 함수를 사용
const objectIs: (x: any, y: any) => boolean =
typeof Object.is === 'function' ? Object.is : is;
export default objectIs;
is 함수
💡 결국 useEffect의 의존성 배열은 Object.is()를 통해 비교하여 값의 변경을 감지하는 것이다!
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('🔥 Effect 실행!');
}, [count]);
}
렌더 1
prevDeps = null(처음 실행이라 비교할 이전 deps가 없음)nextDeps = [0]- →
prevDeps === null이므로false반환 → effect 실행됨
렌더 2 (count가 여전히 0)
prevDeps = [0],nextDeps = [0]- 내부 루프에서
is(0, 0)→true - 전부 같으므로
true반환 → effect 재실행 안 함
렌더 3 (count가 1로 바뀜)
prevDeps = [0],nextDeps = [1]is(1, 0)→false- →
areHookInputsEqual이false반환 → cleanup + effect 재실행