3.1 useEffect란?3.1.1 useEffect의 등장 배경3.1.2 useEffect에 대한 설명과 동작방식3.2 useEffect 사용해보기3.2.1 리렌더링될 때마다 동작하는 useEffect3.2.2 마운트 시점에서만 동작하는 useEffect3.2.3 특정 값이 업데이트 될 때 동작하는 useEffect3.2.4 clean-up 하는 useEffect
3.1 useEffect란?
3.1.1 useEffect의 등장 배경

컴포넌트가 화면에 처음 나타날 때를 ‘마운트(Mount)’라고 하고, 화면에서 사라질 때를 ‘언마운트(Unmount)’ 라고 합니다. 이렇게 컴포넌트가 화면에 나타나서 사라질 때까지의 일생을 컴포넌트의 생명주기라고 합니다.
React 앱이 실행되었을 때, 컴포넌트가 호출되면 마운트 상태에 진입합니다. 마운트 상태가 되면 React는 컴포넌트를 계산해 가상 DOM을 생성합니다. 이렇게 가상 DOM으로 렌더링을 진행한 후, 렌더링된 결과물을 실제 DOM에 업데이트하고 마지막으로 실제 DOM에 업데이트된 내용이 화면에 그려지는 페인팅 과정까지를 밟게 됩니다.
API로 데이터를 요청하는 경우, 일반적인 HTTP통신 작업은 비동기로 처리됩니다. 만약 컴포넌트의 본문 안에 HTTP통신 작업이 존재한다면 리렌더링 될 때마다 작업을 다시 요청하게 될 것입니다. API로 요청한 데이터 크기가 클 경우 브라우저의 페인팅 작업이 늦춰져 리소스를 비효율적으로 운영하게 됩니다.
이러한 부수효과(Side Effects)들은 주요 화면을 렌더링 하는 데에 직접적으로 영향을 주지 않기 때문에, 주요 렌더링 작업과 부수효과를 구분하여 처리하는 것은 버그를 예방하는 데에도 도움을 줄 뿐만 아니라 좋은 사용자 경험을 제공하는 데에 필수입니다. 이러한 부수효과를 처리하기 위한 React Hook이 바로 useEffect입니다.
부수효과(Side Effect) 란?
리액트 공식문서에는 ‘
render()
함수는 순수해야 합니다. 즉, 컴포넌트의 state를 변경하지 않고, 호출될 때마다 동일한 결과를 반환해야 하며, 브라우저와 직접적으로 상호작용을 하지 않습니다.’ 라고 언급합니다. 다시 말하면 render()
함수는 주요 화면의 렌더링만을 담당해야 하며, 그 밖에 컴포넌트의 state를 변경하거나 브라우저와 상호작용하여 호출될 때마다 동일한 결과가 반환되지 않는 것들을 부수효과라고 하는 것입니다. 기존의 클래스형 컴포넌트 방식에서는 생명주기 메서드를 사용해 부수효과를 처리하였습니다. 예시를 통해 살펴보겠습니다.
import React from "react"; class Section1 extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; } componentDidMount() { console.log("화면에 나타남 🙂"); } componentDidUpdate() { console.log("업데이트 발생 😋"); } componentWillUnmount() { console.log("사라집니다 ..."); } render() { return ( <div> <h2>클릭한 횟수: {this.state.count}</h2> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click Me💛 </button> </div> ); } } export default Section1;
componentDidMount
, componentDidUpdate
, componentWillUnmount
는 대표적인 클래스형 컴포넌트의 생명주기 메서드입니다. componentDidMount
는 제일 처음 렌더링 후 돔이 업데이트되는 직후에 실행됩니다. 그리고 버튼을 클릭할 때마다 setState 가 실행되어 리렌더링이 일어나 componentDidUpdate
메서드가 계속 실행되고 있음을 확인할 수 있습니다. componentWillUnmount
는 컴포넌트가 언마운트 되어 DOM에서 제거될 때 실행됩니다. 
useEffect Hook은 이러한 React 클래스형 컴포넌트의 생명주기 메서드인
componentDidMount
, componentDidUpdate
, componentWillUnmount
기능을 하나로 통합한 React Hook입니다. useEffect Hook을 사용하면 React 컴포넌트가 화면에 렌더링 된 이후, 비동기 작업을 처리합니다. 클래스형 컴포넌트는 컴포넌트가 마운트 될 때와 언마운트 될 때가 분기되어 서로 다른 생명주기 메서드에 작성해야 한다는 단점을 가지고 있는 반면, useEffect Hook을 사용한 함수형 컴포넌트의 경우 한 컴포넌트 내부에 관련 코드들을 모아 관리할 수 있어 코드 해석 및 유지보수가 편리하다는 장점이 있습니다. 3.1.2 useEffect에 대한 설명과 동작방식
React 클래스형 컴포넌트의
componentDidMount
, componentDidUpdate
, componentWillUnmount
가 useEffect에서는 어떤 방식으로 구현되는지 살펴보겠습니다. useEffect Hook의 기본 문법은 아래와 같습니다.useEffect(()=> { //작업 내용 (Callback 함수 작성) }, [, depsList])
useEffect는 기본적으로 두 가지 인자를 받습니다. 첫 번째 인자로 작업 내용을 담은 콜백 함수가, 두 번째 인자로 의존성 배열(Array dependencies)이라 불리는 배열이 전달됩니다. 콜백 함수는 작업 내용이므로 반드시 작성해야 하지만, 의존성 배열은 옵션이기 때문에 선택적으로 작성합니다.
의존성 배열에 지정한 상태나 속성이 변경될 경우에 useEffect Hook에 지정한 콜백함수가 실행됩니다. 의존성 배열의 전달값에 따라 useEffect Hook이 어떻게 실행되는지 살펴보겠습니다.
useEffect(()=> { //작업 내용 (Callback 함수 작성) })
첫 번째로 의존성 배열을 전달하지 않은 예시를 살펴보겠습니다.
배열을 사용하지 않는 경우에는
componentDidMount
와 componentDidUpdate
를 합친 것처럼 동작합니다. 처음 렌더링 된 직후에 첫 번째 함수 인자인 콜백함수가 실행되고, props를 받거나 상태가 변경되면 컴포넌트의 리렌더링이 발생해 콜백함수가 실행됩니다. useEffect(()=> { //작업 내용 (Callback 함수 작성) },[])
다음으로 빈 배열을 넣은 경우입니다.
componentDidMount
처럼 동작합니다. 즉, 마운트되고 첫 렌더링이 끝난 직후에만 첫 번째 인자인 함수가 실행됩니다.useEffect(()=> { //작업 내용 (Callback 함수 작성) }, [state, props.a])
위와 같이 배열에 상태나 속성을 지정할 경우에는
componentDidMount
와 componentDidUpdate
를 합친 것처럼 동작하게 됩니다. 처음 렌더링 된 직후(componentDidMount
)에도 첫번째 함수 인자인 콜백함수가 실행되고, 이 배열에 들어있는 어떤 값에 변경이 생겼을 때(componentDidUpdate
)도 첫 번째 인자인 콜백함수가 실행됩니다. 즉, 의존성 배열에 지정한 요소를 관찰하다가 해당 요소의 리렌더링이 발생하는 경우 콜백함수가 실행되는 것입니다. 첫 번째 인자인 콜백함수는 이렇게 배열에 있는 값들에 의존하고 있어, 이 배열을 의존성 배열(Array dependencies) 이라고 합니다.
useEffect(()=> { return () => { //cleanUp } }, [state, props.a])
그럼
componentWillUnmount
는 useEffect Hook으로 어떻게 표현될까요? 바로 이 첫 번째 인자인 콜백함수에 반환값을 넣어주면 됩니다. 컴포넌트가 언마운트 될 때 이 반환된 함수를 실행하게 됩니다. 이렇게 반환되어 실행되는 함수를 정리 함수(clean-up function)라고 합니다. 이는 추후 3.6절에서 상세하게 다뤄보도록 하겠습니다.3.2 useEffect 사용해보기
3.2.1 리렌더링될 때마다 동작하는 useEffect
앞 3.2절에서 useEffect Hook에 대해 간략히 살펴보았습니다. 이번에는 실제 예제 코드와 함께 리렌더링이 될 때마다 동작하는 경우와 마운트 시점에서만 동작하는 경우에 대해 살펴보도록 하겠습니다. 아래는 클릭 이벤트가 발생할 때마다 다시 리렌더링 되는 것을 확인하는 예제 코드입니다.
ex. 클릭 이벤트가 발생할 때마다 다시 리렌더링 되는 것을 확인하는 예
// Section3.js import React, { useEffect, useState } from "react"; const Section3 = () => { const [count, setCount] = useState(0); useEffect(() => { console.log("렌더링 되고 있나요? 🤔"); }); return ( <div> <h2>클릭한 횟수: {count}</h2> <button type="button" onClick={(e) => setCount((count) => count + 1)}> Click Me! </button> </div> ); }; export default Section3;
“Click Me!” 버튼에게 클릭 이벤트를 줄 때마다 count의 값이 1 씩 증가하는 컴포넌트 안에 useEffect Hook을 넣어 useEffect 함수가 동작할 때마다 콘솔창에 “렌더링 되고 있나요? 🤔” 로그가 남도록 하는 예시 코드를 작성하였습니다.
위 코드를 실행하면 최초 렌더링이 되었을 때 브라우저 화면과 콘솔창이 아래 그림 3-3과 같이 나타나게 됩니다.

최초 렌더링이 진행되었을 때, 버튼 클릭 이벤트가 발생하지 않았음에도 불구하고 콘솔창에 “렌더링 되고 있나요? 🤔” 가 찍힌 것을 확인할 수 있습니다. 이는 브라우저 화면이 최초 렌더링 되는 시점에 이미 한 번 useEffect가 동작하였다는 것을 보여줍니다.
이제 처음 버튼을 클릭했을 때 브라우저 화면과 콘솔창을 확인하면 아래와 같이 “렌더링 되고 있나요? 🤔”가 한 번 더 콘솔창에 출력되었음을 아래 그림 3-4를 통해 확인할 수 있습니다. 이는 최초 렌더링 작업에서 useEffect가 동작한 이후, 클릭 이벤트 발생에 따른 count 값이 변경됨에 따라 브라우저에서 컴포넌트를 다시 업데이트한 뒤, 리렌더링하는 과정 속에서 useEffect의 콜백함수가 한 번 더 동작하게 되는 것입니다.

마찬가지로 두 번째 버튼에 클릭 이벤트를 주었을 때 첫 번째와 같이 브라우저 화면과 콘솔창이 예상된 것과 같이 동작함을 아래 그림 3-5를 통해 확인할 수 있습니다.

이처럼 최초 렌더링이 동작한 시점에 useEffect Hook의 콜백함수가 동작되고, 그 이후 버튼에 클릭 이벤트가 발생하여 count 변수의 값이 변경될 때마다 컴포넌트를 리렌더링하게 되면서 useEffect Hook의 콜백함수가 다시금 동작하게 된다는 사실을 확인할 수 있었습니다.
이처럼 useEffect Hook에 두 번째 인자(argument)를 할당하지 않은 경우, useEffect 안의 첫 번째 인자로 주어진 콜백함수는 리렌더링될 때마다 실행된다는 것을 위의 예시를 통해 확인할 수 있었습니다.
다음 3.4절에서는 이 useEffect Hook을 컴포넌트가 최초로 마운트 되었을 시점에서만 콜백함수가 동작하고, 그 이후에는 동작하지 않도록 하는 방법을 확인하도록 하겠습니다.
3.2.2 마운트 시점에서만 동작하는 useEffect
최초 렌더링에서 컴포넌트가 마운트 되는 때에만 useEffect Hook이 동작하도록 하기 위해서는 두 번째 인자인 의존성 배열에 빈 배열을 넣어 주면 됩니다. 이는 해당 useEffect 함수가 컴포넌트 안의 어떠한 것에도 의존하지 않음을 나타내어 줍니다.
다음 아래의 예제 코드를 통하여 자세히 살펴보겠습니다.
아래의 예제 코드에서는 기존의 예제 코드에서 useEffect Hook에 두 번째 인자로 빈 배열만 추가하였습니다.
ex. 최초 브라우저 렌더링 경우(마운트 되는 때)에만 useEffect Hook이 동작하는 예
// Section4.js import React, { useEffect, useState } from "react"; const Section4 = () => { const [count, setCount] = useState(0); useEffect(() => { console.log("렌더링 되고 있나요? 🤔"); }, []); return ( <div> <h2>클릭한 횟수: {count}</h2> <button type="button" onClick={(e) => setCount((count) => count + 1)}> Click Me! </button> </div> ); }; export default Section4;
위와 같이 코드를 작성한 뒤, 브라우저 화면과 콘솔창을 확인하면 아래 그림 3-6과 같음을 확인할 수 있습니다.

앞선 3.3절에서 살펴본 것과 같이 최초 렌더링 시점에서 컴포넌트가 마운트되며 useEffect()가 동작하였음을 확인할 수 있습니다. 이는 우리가 이미 관찰한 바와 같습니다.
이제 버튼을 클릭하여 count 변수의 값을 변경시켜 브라우저가 리렌더링이 되도록 해보겠습니다.

useEffect Hook에 두 번째 인자로 빈 배열을 넣기 전과는 다른 결과가 나타남을 확인할 수 있습니다. 즉 콘솔창에 “렌더링 되고 있나요? 🤔”가 한 번 더 출력되지 않은 것을 확인할 수 있습니다. 이는 useEffect의 콜백함수가 동작하지 않았음을 보여줍니다. 우연일지도 모르니 한 번 더 버튼을 클릭해봅니다.

두 번째 클릭 이벤트가 발생했음에도 불구하고 콘솔창에 “렌더링 되고 있나요? 🤔”가 출력되지 않은 것을 확인할 수 있습니다.
앞서 살펴본 3.3절의 내용과 같이, useEffect Hook에 두 번째 인자(argument)를 할당하지 않은 경우, useEffect 안의 첫 번째 인자로 주어진 콜백함수는 리렌더링될 때마다 실행된다는 것을 3.3절의 예제 코드를 통해 확인할 수 있었습니다.
반면, 이번에는 이 useEffect Hook에 두 번째 인자로 빈 배열을 넣어줌으로써 컴포넌트가 최초로 마운트 되었을 시점에서만 콜백함수가 동작하고, 그 이후에는 동작하지 않게 되었음을 확인하였습니다.
3.2.3 특정 값이 업데이트 될 때 동작하는 useEffect
3.3절과 같이 매번 렌더링 될 때마다 useEffect Hook이 동작하고 콜백함수에 console.log가 아닌 무거운 작업이 반복된다면 성능 저하를 발생 시킬 수도 있고 굉장히 비효율적일 것입니다. 특정 값이 변화할 때만 useEffect Hook을 실행시키고 싶다면 첫 번째 인자에 콜백함수, 두 번째 인자인 의존성 배열에 useState 상태나 props로 받은 값을 넣어주면 됩니다. 두 번째 인자에 값이 포함된 배열이 들어가면 처음 렌더링 될 때, 배열 안의 값이 업데이트 될 때 useEffect Hook이 실행됩니다.
ex. 특정 값이 업데이트 될 때 useEffect Hook이 동작하는 경우
import React, { useState, useEffect } from "react"; const Section5 = () => { const [text, setText] = useState(""); const textChange = (event) => { setText(event.target.value); }; useEffect(() => { console.log("렌더링 되고 있나요?"); }, []); useEffect(() => { console.log("text가 변하고 있나요?"); }, [text]); return ( <div> <input type="text" value={text} onChange={textChange} /> <p>입력한 문구 : {text}</p> </div> ); }; export default Section5;
input 창에 문구를 입력하면 입력한 그대로 화면에 출력되는 코드입니다.

최초 렌더링 시 화면입니다. 콘솔 창에 ‘렌더링이 되고 있나요?’와 ‘text가 변하고 있나요?’라는 문구가 표시되어 있습니다. 컴포넌트가 마운트 될 때 두 개의 useEffect Hook이 정상적으로 동작하고 있다는 것을 알 수 있습니다.

input 창에 hi를 입력했습니다. 두 번째 인자에 빈 배열을 넣어준 첫 번째 useEffect Hook은 더 이상 동작하지 않고 text 값의 변화에 따라 두 번째 useEffect Hook만 h를 입력했을 때 한 번, i를 입력했을 때 한 번 총 두 번 동작하는 것을 볼 수 있습니다. text 값을 조금 더 변화 시켜 보겠습니다.

input 창에 공백과 hello를 추가로 입력했습니다. 그림 3-10와 마찬가지로 ‘text가 변하고 있나요?’라는 문장만 추가로 콘솔 창에 표시되고 있습니다. 이처럼 두 번째 인자에 값을 넣으면 최초 렌더링 때 콜백함수가 한 번 동작하고 React가 두 번째 인자의 상태를 끊임없이 추적하여 상태가 변화할 때 마다 useEffect의 콜백함수가 다시 동작하게 된다는 사실을 확인할 수 있습니다.
3.2.4 clean-up 하는 useEffect
부수 효과가 컴포넌트를 제거할 때 종종 정리가 필요합니다. 이를 위해 컴포넌트가 언마운트되면 더 이상 필요없는 리소스를 정리하는 목적으로 정리함수를 사용합니다. 예를 들어 타이머 기능을 구현하는 경우 타이머 리셋을 위해 이전 기록을 정리해야 합니다. 이 때 정리 함수를 반환하여 이전 작업을 제거할 수 있습니다.
useEffect(()=> { return () => { //cleanUp } }, [state, props.a])
먼저 정리함수를 작성하는 방식은 앞서 살펴보았듯 위 코드와 같습니다. useEffect 내부에서
return
키워드와 함께 반환하고 싶은 정리함수를 넣으면 컴포넌트가 언마운트 된 후 정리함수가 실행됩니다.function Timer() { console.log("타이머 실행 전"); useEffect(() => { const timer = setInterval(() => { console.log("타이머 진행"); }, 1000); return () => { clearIneterval(timer); console.log("타이머 종료"); }; }); return <p>타이머 진행 중</p>; }
위 코드는
setInterval
을 사용하여 1초마다 콘솔에 “타이머 진행”이 나오도록 만든 타이머 함수입니다. 타이머 함수에서 useEffect는 clearIneterval
을 정리 함수에 담아 반환합니다. 만약 정리함수를 반환하지 않는다면 타이머 종료 버튼을 눌러도 종료되지 않습니다. 뿐만 아니라 작업이 계속 누적되어 버튼을 누를 때마다 타이머 실행 간격이 달라집니다. 아래 코드로 버튼을 만들어 정리 함수를 반환할 때와 반환하지 않을 때를 비교해보겠습니다.function App() { const [startTimer, setStartTimer] = useState(false); return ( <> {startTimer && <Timer /> ? ( startTimer && <Timer /> ) : ( <p>타이머 실행 준비</p> )} <button onClick={() => setStartTimer(!startTimer)}>Timer</button> </> ); }
타이머 실행 여부를 확인하기 위해 App 컴포넌트에 버튼을 만들었습니다. 버튼을 눌러 타이머가 시작한다면 아래 사진처럼 브라우저에 “타이머 진행 중”이 뜨고 그렇지 않다면 “타이머 실행 준비”가 나타납니다. 먼저 정리함수를 반환하는 경우 콘솔창을 살펴보겠습니다. 버튼을 누르자 “타이머 진행 전”이 콘솔창에 찍히는 것을 알 수 있습니다. 그 후 타이머가 5초 정도 진행되며 콘솔에 “타이머 진행 중”이 5번 나타납니다. 마지막으로 버튼을 누르면 타이머가 종료되고 그것은 콘솔창에서 확인 가능합니다. useEffect가 정리함수를 반환하면
clearIneterval
로 인해 타이머가 종료되고 1초 간격으로 진행되던 작업은 삭제됩니다. 그래서 버튼을 다시 한 번 더 눌렀을 때 타이머가 정상적으로 1초마다 진행됩니다.


정리함수를 반환하지 않는 사례를 살펴보겠습니다. 아래 코드를 보면 useEffect에서 return으로 반환되는 함수가 없습니다. 그로 인해 컴포넌트가 언마운트 되어도 이전 부수 효과의 기록이 정리되지 않습니다.
function Timer() { console.log("타이머 실행 전"); useEffect(() => { const timer = setInterval(() => { console.log("타이머 진행"); }, 1000); return <p>타이머 진행 중</p>; }
정리함수가 없어도 버튼을 눌렀을 때 브라우저에 나타나는 모습은 동일합니다. 하지만 아래 그림 3-17 콘솔창을 보면 타이머 진행이 멈추지 않고 계속 실행됩니다. 또한 실행 기록이 계속 쌓이기 때문에 타이머 실행 간격이 버튼을 누를 때마다 일정하지 않은 것을 알 수 있습니다. 처음 버튼을 클릭하면 1초마다 타이머가 실행되지만 두 번, 세 번 반복해서 누를 때마다 더 빠른 속도로 콘솔에 “타이머 진행 중”이 나타납니다. 이를 통해 정리함수를 반환하지 않으면 작업이 누적되어 타이머 간격이 달라지는 것을 알 수 있습니다. 결국 정리를 하지 않는 경우 메모리 누수가 발생하여 불필요하게 메모리가 낭비됩니다. 따라서 메모리 누수 방지를 위해서도 정리 과정은 필요합니다.



정리 함수는 클래스와 Hook에서 모두 사용가능합니다. 클래스 컴포넌트에서 정리 함수를 사용할 때는
componentDidMount
에서 기능을 구현하고 componentWillUnmount
에서 정리합니다. 반면 함수 컴포넌트에서는 useEffect Hook이 정리 함수를 반환합니다. 정리를 하기 위해 별도의 부수 효과는 필요하지 않습니다. 부수 효과가 함수를 반환하면 정리가 필요한 시점에 React가 정리 함수를 실행시킵니다. React는 마운트가 해제되고 정리를 시작합니다. 다만 렌더링될 때마다 부수 효과가 실행되기 때문에 React는 다음 부수효과가 실행되기 전까지 이전 렌더링의 정리를 마무리합니다. 타이머 코드를 예로 들어 설명해보겠습니다. function Timer() { useEffect(() => { const timer = setInterval(() => {}, 1000); console.log("마운트 될 때 실행"); return () => { console.log("언마운트 될 때 실행"); clearInterval(timer); }; }); return <p>타이머</p>; }
컴포넌트가 마운트될 때 콘솔에 “마운트 될 때 실행”이 나타납니다. 타이머가 실행되고 버튼을 다시 누르면 콘솔에 "언마운트 될 때 실행"이 나타나고 타이머는 종료됩니다. 결국 함수가 언마운트 되고 나서 정리함수가 반환되는 것을 알 수 있습니다. 아래 그림 3-18 콘솔창을 통해 위 과정이 제대로 실행되었음을 확인 가능합니다.
