Effect와 동기화
일부 컴포넌트는 외부 시스템과 동기화해야 합니다. 예를 들어, React 상태에 따라 비 React 컴포넌트를 제어하거나, 서버 연결을 설정하거나, 컴포넌트가 화면에 표시될 때 분석 로그를 전송하고 싶을 수 있습니다. Effect를 사용하면 렌더링 후 일부 코드를 실행하여 컴포넌트를 React 외부의 시스템과 동기화할 수 있습니다.
학습 내용
- Effect란 무엇인가요?
- Effect가 이벤트와 다른 점
- 컴포넌트에서 Effect를 선언하는 방법
- 불필요한 Effect 재실행을 건너뛰는 방법
- 개발 과정에서 Effect 두 번 실행되는 이유와 해결 방법
Effect란 무엇이며 이벤트와 어떻게 다를까요?
Effects에 대해 알아보기 전에 React 컴포넌트 내부에 있는 두 가지 유형의 로직에 익숙해져야 합니다:
- 렌더링 코드(UI 설명하기에서 소개)는 컴포넌트의 최상위 레벨에 존재합니다. 여기에서 props와 state를 가져와서 변형하고 화면에 표시할 JSX를 반환합니다. 렌더링 코드는 순수해야 합니다. 수학 공식처럼 결과만 계산하고 다른 작업을 수행하지 않아야 합니다.
- 이벤트 핸들러(인터랙티브 기능 추가하기에서 소개)는 컴포넌트 내부에 중첩된 함수로, 계산만 하는 것이 아니라 다른 작업을 수행합니다. 이벤트 핸들러는 입력 필드를 업데이트하거나, 제품 구매를 위한 HTTP POST 요청을 제출하거나, 사용자를 다른 화면으로 안내할 수 있습니다. 이벤트 핸들러에는 특정 사용자 작업(예: 버튼 클릭 또는 입력)으로 인해 발생하는 '부작용'(사이드이펙트- 프로그램의 상태를 변경)이 포함됩니다.
때로는 이것만으로는 충분하지 않습니다. 화면에 표시될 때마다 채팅 서버에 연결해야 하는 ChatRoom 컴포넌트를 생각해 보세요. 서버에 연결하는 것은 순수한 계산이 아니므로(부수적인 효과) 렌더링 중에 발생할 수 없습니다. 그러나 클릭과 같은 특정 이벤트 하나만 있어도 ChatRoom이 표시되는 것은 아닙니다.
Effect를 사용하면 특정 이벤트가 아닌 렌더링 자체로 인해 발생하는 부작용을 지정할 수 있습니다. 채팅에서 메시지를 보내는 것은 사용자가 특정 버튼을 클릭함으로써 직접 발생하므로 이벤트에 해당합니다. 그러나 서버 연결 설정은 컴포넌트가 표시되는 원인이 되는 상호작용에 관계없이 발생해야 하므로 이펙트입니다. 이펙트는 화면이 업데이트된 후 커밋이 끝날 때 실행됩니다. 이때는 React 컴포넌트를 외부 시스템(예: 네트워크 또는 서드파티 라이브러리)과 동기화하기에 좋은 시기입니다.
Note
이 글에서 대문자로 표시된 "Effect"는 위의 React 관련 정의, 즉 렌더링으로 인해 발생하는 부작용을 의미합니다. 더 넓은 프로그래밍 개념을 지칭하기 위해 "부작용(사이드이펙트)"이라고 하겠습니다.
Effect가 필요하지 않을 수도 있습니다.
컴포넌트에 Effect를 추가하는 것을 서두르지 마세요. Effect는 일반적으로 React 코드에서 벗어나 외부 시스템과 동기화할 때 사용된다는 점을 명심하세요. 여기에는 브라우저 API, 서드파티 위젯, 네트워크 등이 포함됩니다. 만약 이펙트가 다른 상태를 기반으로 일부 상태만 조정한다면 Effect가 필요하지 않을 수 있습니다.
Effect 작성 방법
Effect를 작성하려면 다음 세 단계를 따르세요:
- Effect를 선언합니다. 기본적으로 Effect는 렌더링할 때마다 실행됩니다.
- Effect 의존성(dependencies)을 지정합니다. 대부분의 Effect는 매 렌더링 후가 아니라 필요할 때만 다시 실행되어야 합니다. 예를 들어 fade-in 애니메이션은 컴포넌트가 나타날 때만 트리거되어야 합니다. ChatRoom 연결 및 연결 해제는 컴포넌트가 나타났다가 사라질 때 또는 ChatRoom이 변경될 때만 발생해야 합니다. 의존성을 지정하여 이를 제어하는 방법을 배우게 됩니다.
- 필요한 경우 클린업(cleanup)을 추가합니다. 일부 효과는 수행 중이던 작업을 중지, 실행 취소 또는 정리하는 방법을 지정해야 합니다. 예를 들어, "연결"에는 "연결 끊기", "구독"에는 "구독 취소", "가져오기"에는 "취소" 또는 "무시"가 필요합니다. 클린업 함수를 반환하여 이를 수행하는 방법을 배우게 됩니다.
각 단계를 자세히 살펴보겠습니다.
1단계: Effect 선언하기
컴포넌트에서 Effect를 선언하려면 React에서 useEffect Hook을 가져옵니다:
import { useEffect } from 'react';
그런 다음 컴포넌트의 최상위 수준에서 호출하고 Effect 안에 코드를 넣습니다:
function MyComponent() {
useEffect(() => {
// Code here will run after *every* render
});
return <div />;
}
컴포넌트가 렌더링될 때마다 React는 화면을 업데이트한 다음 useEffect 내부에서 코드를 실행합니다. 다시 말해, useEffect는 렌더링이 화면에 반영될 때까지 코드의 실행을 "지연"시킵니다.
Effect를 사용하여 외부 시스템과 동기화하는 방법을 살펴봅시다. <VideoPlayer> React 컴포넌트를 생각해 봅시다. 이 컴포넌트에 isPlaying props를 전달해 재생 또는 일시정지 여부를 제어하면 좋을 것입니다:
<VideoPlayer isPlaying={isPlaying} />;
사용자 지정 비디오 플레이어 컴포넌트는 기본 제공 브라우저 <video> 태그를 렌더링합니다:
function VideoPlayer({ src, isPlaying }) {
// TODO: do something with isPlaying
return <video src={src} />;
}
하지만 브라우저 <video> 태그에는 isPlaying props가 없습니다. 이를 제어할 수 있는 유일한 방법은 DOM 요소에서 play() 및 pause() 메서드를 수동으로 호출하는 것입니다. 동영상이 현재 재생 중인지 여부를 알려주는 isPlaying prop의 값을 play() 및 pause() 등의 호출과 동기화해야 합니다.
먼저 <video> DOM 노드에 대한 참조(ref)를 가져와야 합니다.
렌더링 중에 play() 또는 pause()를 호출하고 싶을 수도 있지만 이는 올바르지 않습니다:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
if (isPlaying) {
ref.current.play(); // Calling these while rendering isn't allowed.
} else {
ref.current.pause(); // Also, this crashes.
}
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
이 코드가 올바르지 않은 이유는 렌더링 중에 DOM 노드로 무언가를 하려고 하기 때문입니다. React에서 렌더링은 JSX의 순수한 계산이어야 하며 DOM 수정과 같은 부작용을 포함하지 않아야 합니다.
게다가 VideoPlayer가 처음 호출될 때, 그 DOM은 아직 존재하지 않습니다! React는 JSX를 반환할 때까지 어떤 DOM을 생성할지 모르기 때문에 play() 또는 pause()를 호출할 DOM 노드가 아직 존재하지 않습니다.
여기서 해결책은 사이드 이펙트를 useEffect로 감싸서 렌더링 계산에서 제외시키는 것입니다:
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
DOM 업데이트를 Effect에 래핑하면 React가 먼저 화면을 업데이트합니다. 그런 다음 Effect가 실행됩니다.
VideoPlayer 컴포넌트가 렌더링될 때(처음 렌더링되거나 다시 렌더링되는 경우) 몇 가지 일이 일어납니다. 먼저, React가 화면을 업데이트하여 <video> 태그가 올바른 props와 함께 DOM에 있는지 확인합니다. 그런 다음 React가 Effect를 실행합니다. 마지막으로 Effect는 isPlaying의 값에 따라 play() 또는 pause()를 호출합니다.
재생/일시정지를 여러 번 누르고 비디오 플레이어가 isPlaying 값에 어떻게 동기화되는지 확인하세요:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
이 예제에서 React state에 동기화한 "외부 시스템"은 브라우저 미디어 API였습니다. 비슷한 접근 방식을 사용해 기존 비 React 코드(예: jQuery 플러그인)를 선언적 React 컴포넌트로 래핑할 수 있습니다.
동영상 플레이어를 제어하는 것은 실제로는 훨씬 더 복잡하다는 점에 유의하세요. play() 호출이 실패할 수도 있고, 사용자가 내장된 브라우저 컨트롤을 사용해 재생하거나 일시정지할 수도 있습니다. 이 예시는 매우 단순하고 불완전합니다.
함정
기본적으로 Effect는 렌더링할 때마다 실행됩니다. 그렇기 때문에 이와 같은 코드는 무한 루프를 생성합니다:
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
Effect는 렌더링의 결과로 실행됩니다. 상태를 설정하면 렌더링이 트리거됩니다. Effect에서 즉시 상태를 설정하는 것은 전원 콘센트를 자체에 꽂는 것과 같습니다. 이펙트가 실행되고, 상태를 설정하면 다시 렌더링이 발생하고, 다시 렌더링이 발생하면 이펙트가 실행되고, 다시 상태를 설정하면 또 다시 렌더링이 발생하는 식입니다.
이펙트는 일반적으로 컴포넌트를 외부 시스템과 동기화해야 합니다. 외부 시스템이 없고 다른 상태를 기반으로 일부 상태만 조정하려는 경우 이펙트가 필요하지 않을 수 있습니다.
2단계: Effect 의존성 지정
기본적으로 Effect는 모든 렌더링 후에 실행됩니다. 하지만 원하지 않는 경우가 종종 있습니다:
- 때로는 느릴 수도 있습니다. 외부 시스템과의 동기화가 항상 즉각적인 것은 아니므로 꼭 필요한 경우가 아니라면 동기화를 건너뛰는 것이 좋습니다. 예를 들어, 키 입력 시마다 채팅 서버에 다시 연결하고 싶지 않을 수도 있습니다.
- 가끔은 잘못된 경우도 있습니다. 예를 들어 키 입력 시마다 컴포넌트 fade-in 애니메이션을 트리거하고 싶지 않을 수 있습니다. 애니메이션은 컴포넌트가 처음 나타날 때 한 번만 재생되어야 합니다.
이 문제를 설명하기 위해 몇 개의 console.log 호출과 부모 컴포넌트의 상태를 업데이트하는 텍스트 입력이 포함된 이전 예제를 살펴봅시다. 입력하면 이펙트가 다시 실행되는 것을 확인할 수 있습니다:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log('Calling video.play()');
ref.current.play();
} else {
console.log('Calling video.pause()');
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
useEffect 호출의 두 번째 인수로 의존성 배열을 지정하여 React가 불필요하게 Effect를 다시 실행하는 것을 건너뛰도록 지시할 수 있습니다. 위의 예제 14줄에 빈 [] 배열을 추가하는 것으로 시작하세요:
useEffect(() => {
// ...
}, []);
React Hook useEffect에 누락된 의존성인 'isPlaying'이 있다는 오류가 표시될 것입니다:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log('Calling video.play()');
ref.current.play();
} else {
console.log('Calling video.pause()');
ref.current.pause();
}
}, []); // This causes an error
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
문제는 이펙트 내부의 코드가 isPlaying prop에 의존하여 수행할 작업을 결정하는데, 이 의존성이 명시적으로 선언되지 않았다는 점입니다. 이 문제를 해결하려면 의존성 배열에 isPlaying을 추가하세요:
이제 모든 의존성이 선언되었으므로 오류가 없습니다. 의존성 배열로 [isPlaying]을 지정하면 React가 isPlaying이 이전 렌더링 때와 동일한 경우 Effect를 다시 실행하지 않도록 건너뛰도록 지시합니다. 이렇게 변경하면 입력을 입력해도 이펙트가 다시 실행되지 않지만 재생/일시정지를 누르면 실행됩니다:
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
console.log('Calling video.play()');
ref.current.play();
} else {
console.log('Calling video.pause()');
ref.current.pause();
}
}, [isPlaying]);
return <video ref={ref} src={src} loop playsInline />;
}
export default function App() {
const [isPlaying, setIsPlaying] = useState(false);
const [text, setText] = useState('');
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => setIsPlaying(!isPlaying)}>
{isPlaying ? 'Pause' : 'Play'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
/>
</>
);
}
의존성 배열은 여러 개의 의존성을 포함할 수 있습니다. React는 지정한 모든 의존성 값이 이전 렌더링에서 가졌던 값과 정확히 동일한 경우에만 Effect를 다시 실행하는 것을 건너뜁니다. React는 Object.is 비교를 사용하여 의존성 값을 비교합니다. 자세한 내용은 useEffect 레퍼런스를 참조하세요.
의존성을 "선택"할 수 없다는 점에 유의하세요. 지정한 의존성이 Effect 내부의 코드에 따라 React가 예상하는 것과 일치하지 않으면 린트 오류가 발생합니다. 이는 코드에서 많은 버그를 잡는 데 도움이 됩니다. 일부 코드가 다시 실행되는 것을 원하지 않는다면 해당 의존성을 "필요"하지 않도록 Effect 코드 자체를 편집하세요.
함정
의존성 배열이 없는 경우와 빈 [] 종속성 배열이 있는 경우의 동작은 다릅니다:
useEffect(() => {
// 렌더링 할 때마다 실행됩니다.
});
useEffect(() => {
// 마운트 시에만 실행됩니다.(컴포넌트가 표시될 때)
}, []);
useEffect(() => {
// 마운트, 그리고 마지막 렌더링 이후 a 또는 b가 변경된 경우에도 실행.
}, [a, b]);
다음 단계에서는 '마운트'가 무엇을 의미하는지 자세히 살펴보겠습니다.
Deep Dive - 의존성 배열에서 참조가 생략된 이유는 무엇인가요?
이 이펙트는 ref와 isPlaying을 모두 사용하지만 종속성으로 선언된 것은 isPlaying뿐입니다:
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
}, [isPlaying]);
이는 ref 객체가 안정적인 아이덴티티를 가지고 있기 때문입니다: React는 모든 렌더링에서 동일한 useRef 호출에서 항상 동일한 객체를 얻을 수 있도록 보장합니다. 절대 변경되지 않으므로 그 자체로 Effect가 다시 실행되지 않습니다. 따라서 포함 여부는 중요하지 않습니다. 포함해도 괜찮습니다:
useState가 반환하는 집합 함수도 안정된 정체성을 가지므로 종속성에서 생략되는 경우가 많습니다. 만약 린터에서 오류 없이 종속성을 생략할 수 있다면, 그렇게 해도 안전합니다.
항상 안정적인 종속성을 생략하는 것은 객체가 안정적이라는 것을 린터가 "확인할" 수 있을 때만 작동합니다. 예를 들어 부모 컴포넌트에서 ref를 전달받았다면 의존성 배열에 명시해야 합니다. 하지만 부모 컴포넌트가 항상 동일한 참조를 전달하는지, 아니면 여러 참조 중 하나를 조건부로 전달하는지 알 수 없기 때문에 이 방법이 좋습니다. 따라서 Effect는 전달되는 참조에 따라 달라집니다.
3단계: 필요한 경우 클린업 추가
다른 예를 생각해 보세요. 채팅 서버가 나타날 때 채팅 서버에 연결해야 하는 ChatRoom 컴포넌트를 작성하고 있다고 가정해 보겠습니다. connect() 및 disconnect() 메서드가 있는 객체를 반환하는 createConnection() API가 주어집니다. 컴포넌트가 사용자에게 표시되는 동안 어떻게 연결 상태를 유지할 수 있을까요?
Effect 로직을 작성하는 것으로 시작하세요:
useEffect(() => {
const connection = createConnection();
connection.connect();
});
재 렌더 시마다 채팅에 연결해 속도가 느려지므로 종속성 배열을 추가합니다:
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
Effect 내부의 코드는 prop이나 state를 사용하지 않으므로 의존성 배열은 [](비어 있음)입니다. 이는 컴포넌트가 "마운트"될 때, 즉 화면에 처음 나타날 때만 이 코드를 실행하도록 React에 지시합니다.
이 코드를 실행해 봅시다:
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
...
export function createConnection() {
// A real implementation would actually connect to the server
return {
connect() {
console.log('✅ Connecting...');
},
disconnect() {
console.log('❌ Disconnected.');
}
};
}
이 효과는 마운트 시에만 실행되므로 콘솔에서 "✅ Connecting..."이 한 번 인쇄될 것으로 예상할 수 있습니다. 하지만 콘솔을 확인하면 "✅ Connecting..."이 두 번 인쇄됩니다. 왜 이런 일이 발생할까요?
ChatRoom 컴포넌트가 다양한 화면이 있는 큰 앱의 일부라고 상상해 보세요. 사용자가 ChatRoom 페이지에서 여정을 시작합니다. 컴포넌트가 마운트되고 connection.connect()를 호출합니다. 그런 다음 사용자가 다른 화면(예: 설정 페이지)으로 이동한다고 상상해 보세요. ChatRoom 컴포넌트가 마운트 해제됩니다. 마지막으로 사용자가 뒤로(Back)를 클릭하면 ChatRoom이 다시 마운트됩니다. 이렇게 하면 두 번째 연결이 설정되지만 첫 번째 연결은 파괴되지 않습니다! 사용자가 앱을 탐색할 때 연결이 계속 쌓이게 됩니다.
이와 같은 버그는 광범위한 수동 테스트 없이는 놓치기 쉽습니다. 이러한 버그를 빠르게 발견할 수 있도록 개발 단계에서 React는 모든 컴포넌트를 최초 마운트 직후에 한 번씩 다시 마운트합니다.
"✅ Connecting..." 로그를 두 번 보면 컴포넌트가 마운트 해제될 때 코드가 연결을 닫지 않는 실제 문제를 발견하는 데 도움이 됩니다.
이 문제를 해결하려면 Effect에서 clean up 함수를 반환하세요:
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, []);
React는 Effect가 다시 실행되기 전에 매번 clean up 함수를 호출하고, 컴포넌트가 마운트 해제(제거)될 때 마지막으로 한 번 더 호출합니다. 정리 함수가 구현되면 어떤 일이 일어나는지 살펴봅시다:
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, []);
return <h1>Welcome to the chat!</h1>;
}
이제 개발 중에 세 개의 콘솔 로그가 표시됩니다:
"✅ Connecting..."
"❌ Disconnected..."
"✅ Connecting..."
이것은 개발에서 올바른 동작입니다. 컴포넌트를 다시 마운트하면 React는 멀리 이동했다가 다시 돌아와도 코드가 손상되지 않는지 확인합니다. 연결을 끊었다가 다시 연결하는 것은 정확히 일어나야 하는 일입니다! clean up을 잘 구현하면 이펙트를 한 번 실행하는 것과 실행하고 정리한 후 다시 실행하는 것 사이에 사용자가 볼 수 있는 차이가 없어야 합니다. React가 개발 과정에서 코드에 버그가 있는지 검사하기 때문에 연결/연결 해제 호출 쌍이 추가됩니다. 이것은 정상적인 현상이니 없애려고 하지 마세요!
프로덕션 환경에서는 "✅ Connecting..."이 한 번만 인쇄됩니다. 컴포넌트를 다시 마운트하는 것은 정리가 필요한 이펙트를 찾는 데 도움이 되도록 개발 중에만 수행됩니다. 엄격 모드를 해제하여 개발 동작을 선택 해제할 수 있지만, 계속 켜두는 것이 좋습니다. 이를 통해 위와 같은 많은 버그를 찾을 수 있습니다.
개발 중에 이펙트가 두 번 실행되는 것을 어떻게 처리할까요?
React는 지난 예제에서와 같이 버그를 찾기 위해 개발 중에 컴포넌트를 의도적으로 다시 마운트합니다. 올바른 질문은 "이펙트를 한 번 실행하는 방법"이 아니라 "어떻게 하면 이펙트를 다시 마운트한 후에도 작동하도록 수정할 수 있는가"입니다.
일반적으로 정답은 clean up 기능을 구현하는 것입니다. 이 기능은 이펙트가 수행 중이던 작업을 중지하거나 실행 취소해야 합니다. 경험상 사용자가 프로덕션에서 이펙트를 한 번 실행하는 경우와, 개발에서 설정(set up) → 정리(clean up) → 설정(set up) 순서를 구분할 수 없어야 한다는 것입니다.
여러분이 작성하게 될 대부분의 Effect는 아래의 일반적인 패턴 중 하나에 해당합니다.
non-React 위젯 제어하기
때때로 React로 작성되지 않은 UI 위젯을 추가해야 할 때가 있습니다. 예를 들어 페이지에 지도 컴포넌트를 추가한다고 가정해 봅시다. 이 컴포넌트에는 setZoomLevel() 메서드가 있고, React 코드의 zoomLevel 상태 변수와 줌 레벨을 동기화하고자 합니다. Effect는 다음과 비슷하게 보일 것입니다:
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
이 경우 clean up이 필요하지 않습니다. 개발 단계에서 React는 Effect를 두 번 호출하지만, 같은 값으로 setZoomLevel을 두 번 호출해도 아무 일도 일어나지 않으므로 문제가 되지 않습니다. 속도가 약간 느려질 수 있지만 프로덕션에서는 불필요하게 다시 마운트되지 않으므로 문제가 되지 않습니다.
일부 API는 연속으로 두 번 호출하지 못할 수도 있습니다. 예를 들어, 내장된 <dialog> 요소의 showModal 메서드는 두 번 호출하면 throw됩니다. clean up 함수를 구현하여 dialog를 닫도록 하세요:
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
개발 시 Effect는 showModal()을 호출한 다음 즉시 close()를 호출한 다음 다시 showModal()을 호출합니다. 이는 프로덕션에서 볼 수 있는 것처럼 showModal()을 한 번 호출하는 것과 동일한 사용자 표시 동작을 갖습니다.
이벤트 구독하기
Effect에서 무언가를 구독하는 경우 clean up 기능에서 구독을 취소해야 합니다:
useEffect(() => {
function handleScroll(e) {
console.log(window.scrollX, window.scrollY);
}
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
개발 시 Effect는 addEventListener()를 호출한 다음, 즉시 removeEventListener()를 호출한 다음 동일한 핸들러로 다시 addEventListener()를 호출합니다. 따라서 한 번에 하나의 활성 구독만 있을 수 있습니다. 이렇게 하면 프로덕션 환경에서와 같이 addEventListener()를 한 번 호출하는 것과 동일한 사용자 가시적 동작이 발생합니다.
애니메이션 트리거
Effect에 애니메이션이 있는 경우 clean up 함수를 사용하면 애니메이션이 초기 값으로 재설정됩니다:
useEffect(() => {
const node = ref.current;
node.style.opacity = 1; // Trigger the animation
return () => {
node.style.opacity = 0; // Reset to the initial value
};
}, []);
개발 단계에서는 불투명도를 1로 설정한 다음 0으로 설정한 다음 다시 1로 설정합니다. 이렇게 하면 프로덕션 환경에서 직접 1로 설정하는 것과 동일한 사용자 표시 동작이 나타납니다. tweening(애니메이션 기법)을 지원하는 타사 애니메이션 라이브러리를 사용하는 경우 정리 함수를 통해 타임라인을 초기 상태로 재설정해야 합니다.
데이터 가져오기
Effect에서 무언가를 가져오는 경우 clean up은 가져오기를 중단하거나 결과를 무시해야 합니다:
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
이미 발생한 네트워크 요청을 '실행 취소'할 수는 없지만, clean up을 사용하면 더 이상 관련이 없는 가져오기가 애플리케이션에 계속 영향을 미치지 않도록 할 수 있습니다. userId가 'Alice'에서 'Bob'으로 변경된 경우, 정리 함수는 'Bob' 이후에 도착하는 'Alice' 응답도 무시하도록 합니다.
개발 시에는 네트워크 탭에 두 개의 페치가 표시됩니다. 이는 잘못된 것이 아닙니다. 위의 접근 방식을 사용하면 첫 번째 Effect가 즉시 정리되어 무시 변수의 복사본이 true로 설정됩니다. 따라서 추가 요청이 있더라도 if(!ignore) 검사 덕분에 상태에 영향을 미치지 않습니다.
프로덕션 환경에서는 요청이 하나만 있을 것입니다. 개발 중 두 번째 요청이 귀찮다면 요청을 중복 제거하고 컴포넌트 간에 응답을 캐시하는 솔루션을 사용하는 것이 가장 좋은 방법입니다:
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
// ...
이렇게 하면 개발 환경이 개선될 뿐만 아니라 애플리케이션이 더 빠르게 느껴집니다. 예를 들어, 사용자가 뒤로 버튼을 누르면 일부 데이터가 캐시되므로 다시 로드될 때까지 기다릴 필요가 없습니다. 이러한 캐시를 직접 빌드하거나 Effects에서 수동 가져오기에 대한 여러 대안 중 하나를 사용할 수 있습니다.
Deep Dive - Effects에서 데이터 가져오기를 대체할 수 있는 좋은 대안은 무엇인가요?
특히 완전한 클라이언트 측 앱에서 데이터를 가져오기 위해 Effects 내에서 가져오기 호출을 작성하는 것은 널리 사용되는 방법입니다. 하지만 이는 매우 수동적인 접근 방식이며 상당한 단점이 있습니다:
- Effects는 서버에서 실행되지 않습니다. 즉, 초기 서버에서 렌더링되는 HTML에는 데이터가 없는 로딩 상태만 포함됩니다. 클라이언트 컴퓨터는 모든 JavaScript를 다운로드하고 앱을 렌더링해야만 이제 데이터를 로드해야 한다는 것을 알게 됩니다. 이는 매우 효율적이지 않습니다.
- Effects에서 직접 가져오기를 사용하면 "네트워크 폭포"를 만들기 쉽습니다. 부모 컴포넌트를 렌더링하면 부모 컴포넌트가 일부 데이터를 가져오고, 자식 컴포넌트를 렌더링하면 자식 컴포넌트가 데이터를 가져오기 시작합니다. 네트워크가 매우 빠르지 않은 경우 모든 데이터를 병렬로 가져오는 것보다 훨씬 느립니다.
- Effect에서 직접 가져오는 것은 일반적으로 데이터를 미리 로드하거나 캐시하지 않는다는 것을 의미합니다. 예를 들어 컴포넌트가 마운트를 해제했다가 다시 마운트하면 데이터를 다시 가져와야 합니다.
- 이는 인체공학적으로 좋지 않습니다. race conditions와 같은 버그가 발생하지 않는 방식으로 fetch 호출을 작성하려면 보일러플레이트 코드가 상당히 많이 필요합니다.
이 단점 목록은 React에만 국한된 것이 아닙니다. 모든 라이브러리를 사용한 마운트에서 데이터를 가져올 때 적용됩니다. 라우팅과 마찬가지로 데이터 불러오기도 제대로 수행하기가 쉽지 않으므로 다음과 같은 접근 방식을 권장합니다:
- 프레임워크(Next.js 같은)를 사용하는 경우 프레임워크에 내장된 데이터 불러오기 메커니즘을 사용하세요. 최신 React 프레임워크에는 효율적이고 위의 함정이 발생하지 않는 통합 데이터 불러오기 메커니즘이 있습니다.
- 그렇지 않은 경우 클라이언트 측 캐시를 사용하거나 빌드하는 것을 고려하세요. 인기 있는 오픈 소스 솔루션으로는 React Query, useSWR, React Router 6.4+ 등이 있습니다. 자체 솔루션을 구축할 수도 있는데, 이 경우 내부적으로 Effects를 사용하되 요청 중복 제거, 응답 캐싱, 네트워크 워터폴 방지(데이터를 미리 로드하거나 데이터 요구 사항을 경로로 끌어올림)를 위한 로직을 추가할 수 있습니다.
이 두 가지 방법 중 어느 것도 적합하지 않은 경우 Effects에서 직접 데이터를 계속 가져올 수 있습니다.
애널리틱스 전송
페이지 방문 시 애널리틱스 이벤트를 전송하는 다음 코드를 살펴보세요:
useEffect(() => {
logVisit(url); // Sends a POST request
}, [url]);
개발 단계에서는 모든 URL에 대해 logVisit이 두 번 호출되므로 이를 수정하고 싶을 수 있습니다. 이 코드는 그대로 유지하는 것이 좋습니다. 이전 예제에서와 마찬가지로 한 번 실행하는 것과 두 번 실행하는 것 사이에 사용자가 볼 수 있는 동작 차이는 없습니다. 실용적인 관점에서 볼 때, 개발 머신의 로그가 프로덕션 메트릭을 왜곡하는 것을 원하지 않으므로 개발 머신에서는 logVisit이 아무 작업도 수행하지 않아야 합니다. 컴포넌트는 파일을 저장할 때마다 다시 마운트되므로 어쨌든 개발 단계에서 추가 방문을 기록합니다.
프로덕션 환경에서는 중복 방문 로그가 발생하지 않습니다.
전송하는 분석 이벤트를 디버깅하려면 앱을 스테이징 환경(프로덕션 모드에서 실행)에 배포하거나, 개발 환경에서 엄격 모드 및 마운트 검사를 일시적으로 선택 해제할 수 있습니다. 또한 Effects 대신 경로 변경 이벤트 핸들러에서 분석을 전송할 수도 있습니다. 보다 정확한 분석을 위해 교차점 옵저버(intersection observers)를 사용하면 뷰포트에 어떤 컴포넌트가 있는지, 얼마나 오래 표시되는지 추적할 수 있습니다.
Effect 없음: 애플리케이션 초기화
일부 로직은 애플리케이션이 시작될 때 한 번만 실행되어야 합니다. 컴포넌트 외부에 배치할 수 있습니다:
if (typeof window !== 'undefined') { // Check if we're running in the browser.
checkAuthToken();
loadDataFromLocalStorage();
}
function App() {
// ...
}
이렇게 하면 브라우저가 페이지를 로드한 후 해당 로직이 한 번만 실행되도록 보장할 수 있습니다.
Effect가 아닌 경우: 제품 구매
clean up 함수를 작성하더라도 사용자가 Effect를 두 번 실행하는 결과를 방지할 방법이 없는 경우가 있습니다. 예를 들어 Effect가 제품 구매와 같은 POST 요청을 전송할 수 있습니다:
useEffect(() => {
// 🔴 틀림: 이 이펙트가 개발 중에 두 번 실행되어 코드에 문제가 노출됩니다.
fetch('/api/buy', { method: 'POST' });
}, []);
제품을 두 번 구매하고 싶지 않을 것입니다. 하지만 이 로직을 효과에 넣으면 안 되는 이유이기도 합니다. 사용자가 다른 페이지로 이동한 후 뒤로 버튼을 누르면 어떻게 될까요? 효과가 다시 실행될 것입니다. 사용자가 페이지를 방문할 때 제품을 구매하는 것이 아니라 사용자가 구매 버튼을 클릭할 때 제품을 구매하기를 원할 것입니다.
구매는 렌더링이 아니라 특정 상호 작용으로 인해 발생합니다. 사용자가 버튼을 누를 때만 실행되어야 합니다. 효과를 삭제하고 /api/buy 요청을 구매 버튼 이벤트 핸들러로 이동합니다:
function handleClick() {
// ✅ 구매는 특정 상호 작용으로 인해 발생하므로 이벤트입니다.
fetch('/api/buy', { method: 'POST' });
}
이는 다시 마운트하면 애플리케이션의 로직이 깨지는 경우 일반적으로 기존 버그가 발견된다는 것을 보여줍니다. 사용자 관점에서 페이지를 방문하는 것은 페이지를 방문하고 링크를 클릭한 후 뒤로 버튼을 누르는 것과 다르지 않아야 합니다. React는 개발 단계에서 컴포넌트를 한 번 다시 마운트하여 이 원칙을 준수하는지 확인합니다.
모든 것을 종합하기
이 플레이그라운드를 통해 Effect가 실제로 어떻게 작동하는지 '느껴볼' 수 있습니다.
이 예에서는 setTimeout을 사용하여 Effect가 실행된 후 3초 후에 입력 텍스트가 포함된 콘솔 로그를 표시하도록 예약합니다. clean up 함수는 보류 중인 타임아웃을 취소합니다. "컴포넌트 마운트"를 눌러 시작합니다:
import { useState, useEffect } from 'react';
function Playground() {
const [text, setText] = useState('a');
useEffect(() => {
function onTimeout() {
console.log('⏰ ' + text);
}
console.log('🔵 Schedule "' + text + '" log');
const timeoutId = setTimeout(onTimeout, 3000);
return () => {
console.log('🟡 Cancel "' + text + '" log');
clearTimeout(timeoutId);
};
}, [text]);
return (
<>
<label>
What to log:{' '}
<input
value={text}
onChange={e => setText(e.target.value)}
/>
</label>
<h1>{text}</h1>
</>
);
}
export default function App() {
const [show, setShow] = useState(false);
return (
<>
<button onClick={() => setShow(!show)}>
{show ? 'Unmount' : 'Mount'} the component
</button>
{show && <hr />}
{show && <Playground />}
</>
);
}
처음에 세 개의 로그가 표시됩니다: Schedule "a" log, Cancel "a" log, 그리고 다시 Schedule "a" log.
3초 후에도 a라는 로그가 표시됩니다. 앞서 배웠듯이, 스케줄/취소 쌍이 추가되는 이유는 React가 개발 중에 컴포넌트를 한 번 다시 마운트하여 정리를 잘 구현했는지 확인하기 때문입니다.
이제 입력을 abc로 편집합니다. 이 작업을 충분히 빠르게 수행하면 Schedule "ab" 로그 바로 뒤에 Cancel "ab" 로그와 Schedule "abc" 로그가 표시됩니다. React는 항상 다음 렌더링의 Effect 전에 이전 렌더링의 Effect를 cleanup합니다. 그렇기 때문에 입력을 빠르게 입력하더라도 한 번에 최대 한 번만 타임아웃이 예약됩니다. 입력을 몇 번 편집하고 콘솔을 보면서 이펙트가 어떻게 정리되는지 느껴보세요.
입력에 무언가를 입력한 다음 즉시 "컴포넌트 마운트 해제"를 누릅니다. 마운트 해제 시 마지막 렌더링의 이펙트가 어떻게 정리되는지 확인해 보세요. 여기서는 실행할 기회를 갖기 전에 마지막 타임아웃을 지웁니다.
마지막으로 위의 컴포넌트를 편집하고 cleanup 기능을 주석 처리하여 시간 초과가 취소되지 않도록 합니다. abcde를 빠르게 입력해 보세요. 3초 후에 어떤 일이 일어날까요? 시간 초과 내에 console.log(text)가 최신 텍스트를 인쇄하고 5개의 abcde 로그를 생성할까요? 직관을 확인하기 위해 한 번 시도해 보세요!
3초 후, 5개의 abcde 로그가 아니라 일련의 로그(a, ab, abc, abcd, abcde)가 표시되어야 합니다. 각 Effect는 해당 렌더링에서 텍스트 값을 '캡처'합니다. 텍스트 상태가 변경되었는지 여부는 중요하지 않습니다. 텍스트 = 'ab'인 렌더링의 Effect는 항상 'ab'로 표시됩니다. 즉, 각 렌더링의 Effect는 서로 분리되어 있습니다. 이것이 어떻게 작동하는지 궁금하다면 클로저에 대해 읽어보세요.
Deep Dive - 각 렌더링에는 고유한 효과가 있습니다.
useEffect는 렌더링 출력에 동작을 "첨부"하는 것으로 생각할 수 있습니다. 이 Effect를 생각해 보세요:
export default function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <h1>Welcome to {roomId}!</h1>;
}
사용자가 앱을 탐색할 때 정확히 어떤 일이 일어나는지 살펴봅시다.
초기 렌더링
사용자가 <ChatRoom roomId="general" />을 방문합니다. 상상으로 roomId를 'general'으로 대체해 봅시다:
// 첫 번째 렌더링용 JSX(roomId = "general")
return <h1>Welcome to general!</h1>;
Effect는 렌더링 출력의 일부이기도 합니다. 첫 번째 렌더링의 Effect가 됩니다:
// 첫 번째 렌더링에 대한 Effect (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// 첫 번째 렌더링에 대한 종속성 (roomId = "general")
['general']
React는 'general' 채팅방에 연결되는 이 이펙트를 실행합니다.
동일한 종속성으로 다시 렌더링하기
<ChatRoom roomId="general" />을 다시 렌더링한다고 가정해 보겠습니다. JSX 출력은 동일합니다:
// 첫 번째 렌더링용 JSX(roomId = "general")
return <h1>Welcome to general!</h1>;
React는 렌더링 출력이 변경되지 않았다고 판단하여 DOM을 업데이트하지 않습니다.
두 번째 렌더링의 효과는 다음과 같습니다:
// 두 번째 렌더링에 대한 Effect (roomId = "general")
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
// 두 번째 렌더링에 대한 종속성 (roomId = "general")
['general']
React는 두 번째 렌더링의 ['general']을 첫 번째 렌더링의 ['general']과 비교합니다. 모든 의존성이 동일하기 때문에 React는 두 번째 렌더링의 Effect를 무시합니다. 호출되지 않습니다.
다른 종속성을 사용하여 다시 렌더링하기
그런 다음 사용자가 <ChatRoom roomId="travel" />을 방문합니다. 이번에는 컴포넌트가 다른 JSX를 반환합니다:
// 세 번째 렌더링용 JSX(roomId = "travel")
return <h1>Welcome to travel!</h1>;
React는 DOM을 업데이트하여 "일반인 환영"을 "여행 환영"으로 변경합니다.
세 번째 렌더링의 효과는 다음과 같습니다:
// 세 번째 렌더링에 대한 Effect (roomId = "travel")
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
},
// 세 번째 렌더링에 대한 종속성(roomId = "travel")
['travel']
React는 세 번째 렌더링의 ['travel']을 두 번째 렌더링의 ['general']과 비교합니다. 하나의 의존성이 다릅니다:
Object.is('travel', 'general')가 거짓입니다. Effect는 건너뛸 수 없습니다.
React가 세 번째 렌더링에서 Effect를 적용하기 전에 마지막으로 실행한 Effect를 정리해야 합니다. 두 번째 렌더링의 Effect가 건너뛰었기 때문에 React는 첫 번째 렌더링의 Effect를 clean up해야 합니다. 첫 번째 렌더링까지 스크롤을 올리면, 그 clean up이 createConnection('general')로 생성된 연결에서 disconnect()를 호출하는 것을 볼 수 있습니다. 그러면 'general' 채팅방에서 앱의 연결이 끊어집니다.
그 후 React는 세 번째 렌더링의 Effect를 실행합니다. 'travel' 채팅방에 연결됩니다.
마운트 해제
마지막으로 사용자가 다른 곳으로 이동하고 ChatRoom 컴포넌트가 마운트 해제된다고 가정해 봅시다. React는 마지막 Effect의 cleanup 함수를 실행합니다. 마지막 이펙트는 세 번째 렌더링에서 나온 것입니다. 세 번째 렌더링의 클린업은 createConnection('travel') 연결을 파괴합니다. 그래서 앱은 'travel' 룸에서 연결을 끊습니다.
개발 전용 동작
엄격 모드가 켜져 있으면 React는 마운트 후 모든 컴포넌트를 한 번 다시 마운트합니다(상태와 DOM은 보존됨). 이렇게 하면 clean up이 필요한 Effect를 쉽게 찾을 수 있고 race conditions과 같은 버그를 조기에 발견할 수 있습니다. 또한 개발 중에 파일을 저장할 때마다 React는 이펙트를 다시 마운트합니다. 이 두 가지 동작은 모두 개발 전용입니다.
요약
- 이벤트와 달리 Effect는 특정 상호작용이 아닌 렌더링 자체로 인해 발생합니다.
- Effect를 사용하면 컴포넌트를 외부 시스템(타사 API, 네트워크 등)과 동기화할 수 있습니다.
- 기본적으로 Effect는 모든 렌더링(초기 렌더링 포함) 후에 실행됩니다.
- 모든 종속성의 값이 마지막 렌더링 때와 같으면 React는 Effect를 건너뜁니다.
- 의존성을 "선택"할 수는 없습니다. Effect 내부의 코드에 의해 결정됩니다.
- 빈 의존성 배열([])은 컴포넌트 "마운트", 즉 화면에 추가되는 것에 해당합니다.
- 엄격 모드에서 React는 컴포넌트를 두 번 마운트하여(개발 시에만!) Effect를 stress-test합니다.
- 만약 Effect를 다시 마운트하는 과정에서 Effect가 깨진다면, clean up 함수를 구현해야 합니다.
- React는 다음 번에 이펙트가 실행되기 전과 마운트 해제 중에 claen up 함수를 호출합니다.
과제 1 / 4: 마운트에 필드 초점 맞추기
이 예제에서는 폼이 <MyInput /> 컴포넌트를 렌더링합니다.
입력의 focus() 메서드를 사용하여 MyInput이 화면에 표시될 때 자동으로 초점을 맞추도록 합니다. 이미 주석 처리된 구현이 있지만 제대로 작동하지 않습니다. 작동하지 않는 이유를 파악하고 수정하세요. (자동 초점 속성에 익숙하다면 동일한 기능을 처음부터 다시 구현하는 것이므로 존재하지 않는다고 가정하세요.)
https://codesandbox.io/s/focus-a-field-on-mount-4drrgp?file=/MyInput.js
과제 2/4: 조건부로 필드에 포커스 지정하기
이 양식은 두 개의 <MyInput /> 컴포넌트를 렌더링합니다.
"양식 표시"를 누르면 두 번째 필드에 자동으로 초점이 맞춰지는 것을 확인할 수 있습니다. 이는 두 <MyInput /> 컴포넌트 모두 내부의 필드에 포커스를 맞추려고 하기 때문입니다. 두 입력 필드에 대해 연속으로 focus()를 호출하면 항상 마지막 입력 필드가 "승리"합니다.
첫 번째 필드에 초점을 맞추고 싶다고 가정해 봅시다. 이제 첫 번째 MyInput 컴포넌트는 true로 설정된 부울 shouldFocus 프로퍼티를 받습니다. MyInput이 수신한 shouldFocus 프로퍼티가 true일 때만 focus()가 호출되도록 로직을 변경합니다.
https://codesandbox.io/s/focus-a-field-conditionally-p9fjiz?file=/MyInput.js
과제 3/4: 두 번 실행되는 interval 수정하기
이 카운터 컴포넌트는 매초마다 증가해야 하는 카운터를 표시합니다. 마운트할 때 setInterval을 호출합니다. 그러면 onTick이 매초마다 실행됩니다. onTick 함수는 카운터를 증가시킵니다.
하지만 1초에 한 번씩 증가하지 않고 두 번 증가합니다. 왜 그럴까요? 버그의 원인을 찾아서 수정하세요.
https://codesandbox.io/s/fix-an-interval-that-fires-twice-kqpkmv?file=/Counter.js
과제 4 중 4: Effect 내부에서 가져오기 수정
이 컴포넌트는 선택한 인물의 약력을 표시합니다. 이 컴포넌트는 마운트할 때와 인물이 변경될 때마다 비동기 함수 fetchBio(person)를 호출하여 약력을 로드합니다. 이 비동기 함수는 결국 문자열로 해석되는 Promise를 반환합니다. 가져오기가 완료되면 setBio를 호출하여 선택 상자 아래에 해당 문자열을 표시합니다. 이 코드에 버그가 있습니다. 먼저 '앨리스'를 선택합니다. 그런 다음 "Bob"을 선택한 다음 바로 뒤에 "Taylor"를 선택합니다. 이 작업을 충분히 빠르게 수행하면 해당 버그를 발견할 수 있습니다: 테일러가 선택되었지만 아래 단락에 "이것은 밥의 약력입니다."라고 표시됩니다.
왜 이런 일이 발생하나요? 이 Effect 내부의 버그를 수정합니다.
https://codesandbox.io/s/fix-fetching-inside-an-effect-47gvk1?file=/App.js
발표요약)
1. useEffect 내 코드는 모든 렌더링 직후 실행한다.
2. 의존성 배열(디펜던시)에 선언된 값이 이전 렌더링과 같으면 Effect를 생략한다. 다시말하면, 디펜던시에 선언되어있는 값이 변경되면 useEffect내 코드를 실행한다.
3. 의존성 배열을 빈배열로 두면 마운트 시에 1회만 실행한다.
4. 클린업 함수는 다음 Effect가 실행되기 전에 호출한다. 따라서 이벤트를 제거하거나 하는 용도로 사용한다.
Q. 궁금한점이 있는데, 과제 3번같은 경우에서, setInterval 는 엄격모드에서만 2번호출하여 간격이 2로 조정된다. 그래서 clearInterval을 하도록 하는데, 이게 prod 모드에서는 1번 호출하니까 문제가 없는건지? 아니면 interval이 제거되지 않은 상태이므로 문제가 있는건지 궁금하다. 예를들자면 이벤트리스너의 경우, 계속 이벤트리스너가 add되어 쌓이는 메모리 문제가 발생할수있을까?
A. 대부분의 최신 브라우저는 자체적으로 이벤트리스너를 제거함으로 메모리 누수가 없다.
출처)
https://stackoverflow.com/questions/6033821/do-i-need-to-remove-event-listeners-before-removing-elements/37096563#37096563