Skip to main content

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
  • areHookInputsEqualfalse 반환 → cleanup + effect 재실행