Search

useEffect

useEffect()는 함수형 컴포넌트에서 사이드 이펙트(side effect)를 발생시키는데 사용되는 리액트 Hook입니다. 사이드 이펙트란 프로그래밍에서 처음 의도한 것 외에 발생하는 부수적인 효과를 의미합니다. useEffect()를 사용하면 컴포넌트가 상태 변화 및 랜더링 과정에 따라 여러가지 부수적인 효과들이 발생하도록 할 수 있습니다.

useEffect의 사용

useEffect()는 아래 형태로 사용됩니다.
useEffect(() => {/* 수행할 부수적인 작업 */}, [/* 의존성 배열 */]);
JavaScript
복사
첫번째 인자는 이펙트가 발생할때 실행하는 함수이고, 두번째 인자는 이펙트를 발생시킬 조건을 지정하는 의존성 배열(dependency array)입니다. 의존성 배열의 조건에 따라 첫번째 인자로 지정한 함수가 실행이 될지, 실행이 되지 않을지에 대한 여부가 결정됩니다.

의존성 배열이 없는 경우

의존성 배열을 전혀 지정하지 않으면 매번 컴포넌트가 랜더링 될 때마다 이펙트 함수가 실행됩니다.
이전 useState 편에서 보았던 카운터 앱 예제에서 의존성 배열이 없는 useEffect()를 추가해 결과를 한번 확인해 봅시다. 이펙트 함수에서 수행할 작업은 단순하게 콘솔에 로그를 남기는 것만 추가하겠습니다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const Counter = () => { const [count, setCount] = useState(0); // 의존성 배열 없이 콘솔에 로그를 남기는 이펙트 추가 useEffect(() => { console.log('✨ Effect 발생!', '현재 카운트:', count); }); const increase = (value) => { setCount((currentCount) => { return currentCount + value; }); }; return ( <> <h1>현재 카운트: {count}</h1> <button onClick={() => increase(1)}> + </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
개발자 모드의 콘솔창과 함께 확인해보면, 첫 랜더링때 한번 로그가 남고 이후로 카운트 상태 변경과 동시에 로그가 남겨지는 것을 확인할 수 있습니다.
상태가 변경되면 컴포넌트는 랜더링을 다시 수행합니다. 의존성 배열이 없이 선언된 useEffect()의 경우 컴포넌트가 랜더링 작업을 수행할 때마다 무조건 이펙트 함수를 실행합니다.
가상 DOM에 컴포넌트가 추가되고, 최초로 랜더링 되는 과정을 마운트(mount)라고 합니다. 이때에도 기본적으로 이펙트 함수가 실행됩니다. 그래서 화면을 실행하자마자 콘솔에 이미 로그가 한번 찍혀있는 것을 확인할 수 있습니다.

의존성 배열이 빈 배열인 경우

컴포넌트가 마운트되는 경우에만 이펙트 함수가 실행되고 이후엔 전혀 실행되지 않기를 원한다면 useEffect()의 의존성 배열을 빈 배열[]로 선언하면 됩니다. 컴포넌트의 마운트 과정 이후 이펙트가 의존하고 있는 다른 값이 전혀 값이 없기 때문에, 어떤 경우에도 이펙트 함수가 다시 실행되지 않습니다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const Counter = () => { const [count, setCount] = useState(0); useEffect(() => { console.log('✨ Effect 발생!', '현재 카운트:', count); }, []); // 의존성 배열에 빈 배열 지정 const increase = (value) => { setCount((currentCount) => { return currentCount + value; }); }; return ( <> <h1>현재 카운트: {count}</h1> <button onClick={() => increase(1)}> + </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
콘솔에 마운트때 남겨진 로그 외에는 count 상태가 변경 되더라도 다시 로그가 남지 않습니다.

의존성 배열에 값을 지정한 경우

특정 값이 변경될때만 이펙트가 실행되길 원한다면 그 값을 의존성 배열에 추가하면 됩니다. 비교 확인을 위해 카운터 앱에 텍스트 컬러를 결정하는 color 상태를 추가하고, 두개의 useEffect()를 선언해 각각 count 상태와 color 상태를 의존성 배열에 하나씩 포함시키겠습니다. 그리고 컬러 상태를 변경하는 버튼을 추가한 뒤, 각각의 상태 변화에 따른 이펙트 실행 여부를 비교해 봅시다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const Counter = () => { const [count, setCount] = useState(0); // h1의 컬러를 결정하는 상태 추가 const [color, setColor] = useState('blue'); // count 변경될때만 이펙트 실행 useEffect(() => { console.log('➕ 카운트 변경!', '현재 카운트:', count); }, [count]); // color 변경될때만 이펙트 실행 useEffect(() => { console.log('🎨 컬러 변경!', '현재 컬러:', color); }, [color]); const increase = (value) => { setCount((currentCount) => { return currentCount + value; }); }; return ( <> <h1 style={{ color }}>현재 카운트: {count}</h1> <button onClick={() => increase(1)}> + </button> <button onClick={() => setColor('blue')}> 파란색 </button> <button onClick={() => setColor('red')}> 빨간색 </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
의존성 배열에 값을 지정하면 마운트 이후 지정한 값이 변경될때만 이펙트 함수를 실행합니다.
의존성 배열에 하나의 값만 지정하는 것은 아닙니다. 배열이기 때문에 두개 이상의 값을 지정하는 것도 물론 가능합니다. 값이 두개 이상 지정된 경우, 지정된 값 중 어느 하나라도 변경이 감지되면 이펙트가 발생합니다. 이번에는 이전 코드에 fontSize 상태를 추가하고, 두번째 useEffect()의 의존성 배열에 color와 함께 fontSize를 추가해서 결과를 확인해봅시다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const Counter = () => { const [count, setCount] = useState(0); const [color, setColor] = useState('blue'); // h1의 폰트 사이즈를 결정하는 상태 추가 const [fontSize, setFontSize] = useState('2em'); // count 변경될때만 이펙트 실행 useEffect(() => { console.log('➕ 카운트 변경!', '현재 카운트:', count); }, [count]); // color, fontSize 중 하나라도 변경되면 이펙트 실행 useEffect(() => { console.log( '🖌️ 텍스트 스타일 변경!', '현재 스타일:', color, fontSize ); }, [color, fontSize]); const increase = (value) => { setCount((currentCount) => { return currentCount + value; }); }; return ( <> <h1 style={{ color, fontSize }}>현재 카운트: {count}</h1> <button onClick={() => increase(1)}> + </button> <button onClick={() => setColor('blue')}> 파란색 </button> <button onClick={() => setColor('red')}> 빨간색 </button> <button onClick={() => setFontSize('1em')}> 1em </button> <button onClick={() => setFontSize('2em')}> 2em </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사
의존성 배열에 상태(state)를 사용한 것만 예시로 보여드렸지만 props도 의존성 배열에 추가해서 사용할 수 있습니다.

useEffect를 사용할때 무한 루프에 빠지지 않게 조심!

useEffect()를 자칫 잘못 사용하게 되면, 앱이 무한 루프에 빠지게 될 수 있습니다. useEffect()의 이펙트 함수 안에서 상태 변경을 처리하거나 다른 useEffect()의 의존성에 포함되어 있는 값을 동적으로 수정하려고 할때 이런 경우가 발생할 수 있는데요. 아래 코드처럼 이펙트→상태변경→이펙트→상태변경과 같은 무한 루프에 빠지지 않도록 주의해야 합니다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const Counter = () => { const [count, setCount] = useState(0); // 🙅🏻‍♂️ count가 다른 값 변경될때 다시 count를 또 다른 값으로 // 변경하게 되므로 앱이 무한루프에 걸리게 됩니다. useEffect(() => { increase(1); }, [count]); const increase = (value) => { setCount((currentCount) => { return currentCount + value; }); }; return ( <> <h1>현재 카운트: {count}</h1> <button onClick={() => increase(1)}> + </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<Counter />);
JavaScript
복사

useEffect 정리(clean-up)

컴포넌트가 가상 DOM에서 제거되고 화면에서 사라지는 것을 언마운트(unmount)라고 합니다. 컴포넌트가 언마운트가 되면 컴포넌트에서 useEffect()로 생성한 부수적인 효과도 함께 사라져야 하지만, 몇몇 경우는 컴포넌트가 삭제되더라도 메모리에 그대로 남아있는 경우가 있습니다. 이런 경우 메모리 누수(memory leak)로 인해 성능 저하가 발생하여 앱이 버벅이는 등의 안좋은 사용자 경험(UX)를 일으킬 수 있고, 예상하지 못한 에러가 발생할 수도 있기 때문에 컴포넌트가 언마운트될때 메모리에 남지 않도록 useEffect()를 잘 정리(clean-up)를 해주는 것이 필요합니다.
모든 useEffect() 사용에 반드시 정리(clean-up)를 해줄 필요는 없지만, 아래의 경우에는 가급적 정리 코드와 함께 사용해주는 것이 좋습니다.
1.
setTimeout(), setInterval()과 같은 타임 함수 사용
2.
Websocket 연결
3.
addEventListener()로 이벤트 리스너를 추가하는 경우
이에 대한 예로, useEffect()에서 setInterval()을 사용할때 정리를 해주지 않으면 어떤 문제가 발생하는지를 아래 코드로 한번 확인해 봅시다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; // Timer 컴포넌트 const Timer = () => { // clean-up 하지 않고 setInterval() 사용 useEffect(() => { // 1초마다 현재시간을 보여주는 타이머 const timer = setInterval(() => { console.log( '⏱️ 현재시간:', (new Date()) .toISOString() .slice(0, 19) .replace(/-/g, '/') .replace('T', ' ') ); }, 1000); }, []); return <h1>콘솔에서 현재시간 로그를 확인하세요.</h1>; } // App 컴포넌트 const App = () => { const [isShow, setIsShow] = useState(true); return ( <> {/* isShow 상태가 true일때만 Timer 컴포넌트 마운트 */} {isShow && <Timer />} <button onClick={() => setIsShow(true)}> 사용 </button> <button onClick={() => setIsShow(false)}> 삭제 </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<App />);
JavaScript
복사
정리(clean-up)를 해주지 않아 컴포넌트가 언마운트 되더라도 계속 타이머가 작동중인 것을 확인할 수 있습니다. 여기서 같은 컴포넌트가 다시 마운트되면 타이머가 중복으로 실행됩니다.
useEffect()에서 정리를 사용하는 방법은 간단합니다. 아래처럼 이펙트를 정리하는 함수를 반환(return)문에 포함시켜주면 됩니다.
... const Timer = () => { useEffect(() => { const timer = setInterval(() => { console.log( '⏱️ 현재시간:', (new Date()) .toISOString() .slice(0, 19) .replace(/-/g, '/') .replace('T', ' ') ); }, 1000); // 타이머를 제거하는 함수를 리턴하여 컴포넌트가 언마운트될때 함께 정리되도록 함 return () => clearTimeout(timer); }, []); ...
JavaScript
복사
정리 코드를 추가해서 확인해보면 전에 보았던 문제가 더 이상 발생하지 않습니다.
import { useState, useEffect } from 'react'; import ReactDOM from 'react-dom/client'; const Timer = () => { useEffect(() => { const timer = setInterval(() => { console.log( '⏱️ 현재시간:', (new Date()) .toISOString() .slice(0, 19) .replace(/-/g, '/') .replace('T', ' ') ); }, 1000); return () => clearTimeout(timer); }, []); return <h1>콘솔에서 현재시간 로그를 확인하세요.</h1>; } const App = () => { const [isShow, setIsShow] = useState(true); return ( <> {isShow && <Timer />} <button onClick={() => setIsShow(true)}> 사용 </button> <button onClick={() => setIsShow(false)}> 삭제 </button> </> ); }; const rootNode = document.getElementById('root'); ReactDOM.createRoot(rootNode).render(<App />);
JavaScript
복사

 useState

 useContext

React Hooks 목록