일부 컴포넌트에서는 외부 시스템과 동기화해야 할 수도 있다.
Effect를 사용하면 렌더링 후 특정 코드를 실행하여 React 외부의 시스템과 컴포넌트를 동기화할 수 있다.
화면에 보일 때마다 채팅 서버에 접속해야 하는 ChatRoom 컴포넌트 예시:
서버에 접속하는 것 -> 부수 효과를 발생시키기 때문에 렌더링 중에는 할 수 없다.
클릭 한 번으로 ChatRoom이 표시되는 특정 이벤트는 하나도 없다.
Effect -> 렌더링 자체에 의해 발생하는 부수 효과를 특정하는 것
- 특정 이벤트가 아닌 렌더링에 의해 직접 발생
채팅에서 메시지를 보내는 것 -> 이벤트(사용자가 특정 버튼을 클릭함에 따라 직접적으로 발생)
서버 연결 -> Effect(컴포넌트의 표시를 주관하는 어떤 상호 작용과도 상관없이 발생해야 한다.)
Effect는 커밋이 끝난 후에 화면 업데이트가 이루어지고 나서 실행된다.
React에서 useEffect 훅을 import
import { useEffect } from 'react';
컴포넌트의 최상의 레벨에서 호출하고 Effect 내부에 코드를 넣음
function MyComponent() {
useEffect(() => {
// 이곳의 코드는 모든 렌더링 후에 실행된다.
});
return <div />;
}
컴포넌트가 렌더링될 때마다 React -> 화면 업데이트한 다음 useEffect 내부의 코드 실행
useEffect는 화면에 렌더링이 반영될 때까지 코드 실행을 "지연"시킴
<VideoPlayer isPlaying={isPlaying} />;
isPlaying이라는 props를 통해 재생 중인지 일시 정지 상태인지 제어
function VideoPlayer({ src, isPlaying }){
return <video src={src} />;
}
₩<video₩> 태그에는 isPlaying prop이 없다.
DOM 요소에서 수동으로 play() 및 pause() 메소드를 호출해야 한다.
isPlaying prop의 값을 play() 및 pause()와 같은 호출과 동기화해야 한다.
먼저 ₩<video₩> DOM 노드의 ref를 가져온다.
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }){
const ref = useRef(null);
if(isPlaying){
ref.current.play();
} else {
ref.current.pause();
}
return <video ref={ref} src={src} loop playInline />;
}
export default function App(){
const [isPlaying, setIsPlaying] = useState(false);
return (
<>
<button onClick={()=>setIsPlaying(!isPlaying)}>
{isPlaying ? : '일시정지' : '재생'}
</button>
<VideoPlayer
isPlaying={isPlaying}
src="https://"
/>
</>
);
}
-> 이 코드는 올바르지 않다.
렌더링 중에 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 src={src} ref={ref} loop playInline />;
}
DOM 업데이트를 Effect로 감싸면 React가 화면을 업데이트한 다음에 Effect가 실행된다.
VideoPlayer 컴포넌트가 렌더링 될 때(처음 호출하거나 다시 렌더링 할 때)
React는 화면을 업데이트하여 video 태그가 올바른 속성과 함께 DOM에 있는지 확인
그런 다음 React는 Effect를 실행
마지막으로 Effect에서는 isPlaying 값에 따라 play() 또는 pause()를 호출
이 예시에서 React 상태와 동기화된 외부 시스템 -> 브라우저 미디어 API
React가 아닌 레거시 코드(ex: JQuery 플러그인)를 선언적인 React 컴포넌트로 감싸는 데에 사용 가능
=> 예시는 매우 단순화된 것이다.
play()를 호출하는 것이 실패될 수도 있다.
기본적으로 Effect는 모든 렌더링 후에 실행된다.
이거 때문에 다음 코드는 무한 루프가 생길 것이다.
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1);
});
state를 설정하면 렌더링이 트리거된다.
Effect 안에서 상태를 설정하는 것 -> Effect가 실행되고 상태가 설정되면 재렌더링이 발생, Effect가 다시 실행되고 상태가 설정되면 또 다른 재렌더링 발생
Effect는 컴포넌트를 외부 시스템과 동기화하는 데 사용되기에 외부 시스템잉 없고 다른 상태에 기반하여 상태를 조정하려는 경우엔 Effect가 필요하지 않을 수 있다.
Effect는 기본적으로 모든 렌더링 후에 실행된다.
예시 - console.log 호출과 부모 컴포넌트 상태를 업데이트하는 텍스트 입력
import { useState, useRef, useEffect } from 'react';
function VideoPlayer({ src, isPlaying }){
const ref = useRef(null);
useEffect(() => {
if(isPlaying){
console.log('video play() call');
ref.current.play();
} else {
console.log('video pause() call');
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playInline />;
}
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 ? '일시 정지' : '재생'}
</button>
<VideoPlayer
isPlaying={isPlaying{
src="https://"
/>
</>
);
}
input에 text를 입력하면 계속 Effect가 실행된다.
React에게 Effect를 불필요하게 다시 실행되지 않도록 지시하려면 useEffect 호출의 두 번째 인자로 의존성(dependencies) 배열을 지정한다.
useEffect(()=> {
// ...
}, []);
빈 배열을 추가한다.
'isPlyaing'에 대한 의존성이 누락되었다는 오류가 뜰 것이다.
Effect 내부의 코드가 어떤 작업을 수행할지 결정하기 위해 isPlayaing prop에 의존하지만 이 의존성이 명시적으로 선언되지 않았다.
-> 의존성 배열에 isPlaying을 추가해야 한다.
useEffect(()=>{
if(isPlaying){
} else {
}
}, [isPlaying]);
의존성 배열로 [isPlaying]을 지정하면 React에게 이전 렌더링 중에 isPlaying이 이전과 동일하다면 Effect를 다시 실행하지 않도록 해야 한다고 알려준다.
이제 입력란에 텍스트를 입력하면 Effect가 다시 실행되지 않고, 재생/일시정지 버튼을 누르면 Effect가 실행된다.
의존성 배열에는 여러 개의 종속성을 포함 가능하다.
React -> 지정한 모든 종속성이 이전 렌더링의 그것과 정확히 동일한 값을 가진 경우에만 Effect를 다시 실행하지 않는다.
React는 Object.js 비교를 사용하여 종속성 값을 비교한다.
의존성을 선택할 수 없다.
의존성 배열에 지정한 종속성이 Effect 내부의 코드를 기반으로 React가 기대하는 것과 일치하지 않으면 린트 에러 발생
의존성 배열이 없는 경우와 빈 의존성 배열 []이 있는 경우 동작이 다르다.
useEffect(() => {
// 모든 렌더링 후에 실행됨
});
useEffect(()=> {
// 마운트될 때만 실행됨(컴포넌트가 나타날 때)
}, []);
useEffect(() => {
// 마운트될 때 실행되며, *또한* 렌더링 이후에 a, b 중 하나라도 변경된 경우에도 실행됨
}, [a, b]);
ref 객체가 안정된 식별성(stable identity)를 가지기 때문.
React는 동일한 useRef 호출에서 항상 같은 객체를 얻을 수 있음을 보장함.
이 객체는 절대 변경되지 않기 때문에 자체적으로 Effect를 다시 실행시키지 않는다.
따라서 ref는 의존성 배열에 포함하든 말든 상관 없다.
useState로 반환되는 set 함수들도 안정된 식별성을 가지기 때문에 의존성에서 생략되는 것을 볼 수 있다.
안정된 식별성을 가진 의존성을 생략하는 것은 린터가 해당 객체가 안정적임을 알 수 있는 경우에만 작동한다.
예시)
ref가 부모 컴포넌트에 전달되었다면, 의존성 배열에 명시해야 함.
-> 부모 컴포넌트가 항상 동일한 ref를 전달하는지 또는 여러 ref 중 하나를 조건부로 전달하는지 알 수 없기 때문이다.
예시) 사용자에게 표시될 때 채팅 서버에 연결해야 하는 ChatRoom 컴포넌트
createConnection() API가 주어짐
-> connect(), disconnect() 메소드를 가진 객체를 반환
사용자에게 표시되는 동안 컴포넌트가 채팅 서버와의 연결을 유지하려면:
useEffect(()=>{
const connection = createConnection();
connection.connect();
});
매번 재렌더링 후에 채팅 서버에 연결하는 것은 느리므로 의존성 배열 추가:
useEffect(()=>{
const connection = createConnection();
connection.connect();
}, []);
Effect 내부의 코드는 어떠한 props나 상태도 사용하지 않으므로 의존성 배열은 빈 배열이다.
-> 컴포넌트가 마운트될 때만 실행하도록 함 -> 화면에 처음으로 나타날 때에만 실행되게 함
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
}, []);
return <h1>채팅에 오신걸 환영합니다!</h1>;
}
-> console에 연결 중...이 2번 찍힌다.
ChatRoom 컴포넌트가 여러 화면으로 구성된 큰 앱의 일부라고 가정
사용자가 ChatRoom 페이지에서 여정 시작
컴포넌트가 마운트되고 connection.connect()를 호출
그런 다음 사용자가 다른 화면으로 이동 -> ChatRoom 컴포넌트 마운트 해제
사용자가 뒤로 가기 버튼 클릭 -> ChatRoom 다시 마운트
=> 문제를 빠르게 파악할 수 있도록 React는 개발 모드에서 초기 마운트 후 모든 컴포넌트를 한 번 다시 마운트
한다.
문제를 해결하려면:
useEffect(()=>{
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
};
}, [])'
React는 Effect가 다시 실행되기 전마다 클린업 함수를 호출
컴포넌트가 마운트 해제될 때도 마지막으로 호출
import { useEffect, useState } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
const connection = createConnection();
connection.connect();
return () => {
connection.disconnect();
}
}, []);
return <h1>채팅에 오신 걸 환영합니다!</h1>;
}
console
개발 중에는 연결/해제 호출이 하나 더 있다 -> React가 개발 중에 코드를 검사하여 버그를 찾는 것
정상적인 동장이다.
배포 환경에서는 연결 중...이 한 번만 출력된다.
컴포넌트를 다시 마운트하는 것은 개발 중에만 발생하며 클린업이 필요한 Effect를 찾아주는 데 도움을 준다.
어떻게 Effect가 다시 마운트된 후에도 작동하도록 고칠 것인가
-> 정답: 클린업 함수를 구현하는 것
클린업 함수 - Effect가 수행하던 작업을 중단하거나 되돌리는 역할
기본 원칙: 사용자가 Effect가 한 번 실행되는 것과 설정->클린업->설정 순서 간에 차이를 느끼지 못해야 한다.
ref를 사용해 Effect가 한 번만 실행되도록 하는 것 금지
const connectionRef = useRef(null);
useEffect(()=>{
if(!connectionRef.current){
connectionRef.current = createConnection();
connectionRef.curent.connect();
}
}, []);
이렇게 하면 개발 모드에서 연결 중...이 한 번만 보이지만 버그가 수정된 건 아니다.
사용자가 다른 곳에 가도 연결이 끊어지지 않고
다시 사용자가 돌아왔을 때 새로운 연결이 생성 -> 연결이 계속 쌓임
종종 React로 작성되지 않은 UI 위젯을 추가해야 할 때가 있다.
ex) 페이지에 지도 컴포넌트 추가
지도 컴포넌트의 setZoomLevel() 메소드 -> zoomLevel state와 동기화하려 할 것
useEffect(() => {
const map = mapRef.current;
map.setZoomLevel(zoomLevel);
}, [zoomLevel]);
이 경우에는 클린업이 필요 없다.
개발 모드에서 React는 Effect를 두 번 호출하지만 동일한 값(zoomLevel 값이 변하지 않음)을 가지고 setZoomLevel을 두 번 호출하는 건 문제가 되지 않는다.
일부 API는 연속 두 번 호출을 허용하지 않는다. -> ex) dialog 요소의 showModal 메소드는 두 번 호출하면 예외를 던짐
useEffect(() => {
const dialog = dialogRef.current;
dialog.showModal();
return () => dialog.close();
}, []);
개발 중에는 Effect가 showModal()을 호출한 다음 즉시 close()를 호출하고 다시 showModal()을 호출
=> 제품 환경에서 볼 수 있는 것과 동일
만약 Effect가 어떤 것을 구독한다면, 클린업 함수에서 구독을 해지해야 한다.
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가 어떤 요소를 애니메이션으로 표시하는 경우
-> 클린업 함수에서 애니메이션을 초기 값으로 재설정해야 함
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가 어떤 데이터를 가져온다면, 클린업 함수에서는 fetch를 중단하거나 결과를 무시해야 함
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if(!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
🧨 문제: race condition
예를 들어 이런 일이 발생할 수 있어요:
userId = "alice"일 때 effect 실행 → fetchTodos("alice") 요청 보냄
요청이 끝나기도 전에 → userId = "bob"로 바뀜
React가 effect 클린업 실행 → ignore = true 설정
fetchTodos("alice") 결과가 늦게 도착함
클린업이 없다면 → setTodos(alice의 데이터)가 호출됨! 😱
이러면 화면에 "bob의 UI인데, alice의 데이터"가 보이는 이상한 상황이 됨
이미 발생한 네트워크 요청을 실행 취소할 수는 없다.
클린업 함수는 더이상 관련이 없는 패치가 애플리케이션에 계속 영향을 미치지 않도록 해야 한다.
userId가 Alice에서 Bob으로 변경되면 클린업은 Bob 이후에 도착하더라도 Alice 응답을 무시하도록 보장한다.
개발 중에는 네트워크 탭에 두 개의 패치가 표시됨
그러나 문제 없다.
첫 번째 Effect는 즉시 클린업되어 ignore 변수의 복사본이 true로 설정된다.
-> 추가 요청이 있더라도 if(!ignore) 검사 덕분에 state에 영향을 미치지 않는다.
제품 환경에서는 하나의 요청만 있을 것
두 번째 요청이 문제가 되면 가장 좋은 방법은 중복 요청을 제거하고 컴포넌트 간에 응답을 캐시하는 솔루션을 사용하는 것
funcion TodoList() {
const todos = useSomeDataLibrary('/api/user/${userId}/todos');
// ...
}
-> 개발 환경 개선, 애플리케이션 반응 속도 향상
ex) 사용자가 뒤로 가기 버튼을 눌렀을 때 데이터를 다시 로드하는 것을 기다릴 필요가 없다. (데이터가 캐시되기 때문)
단점
다음 방식을 권장
페이지 방문 시 분석 이벤트를 보내는 코드:
useEffect(() => {
logVisit(url); // POST 요청을 보냄
}, [url]);
개발 환경에서는 logVisit가 각 URL에 대해 두 번 호출될 것
-> 이 코드를 그대로 유지하는 것을 권장
컴포넌트는 파일을 저자할 때마다 재마운트되므로 개발 환경에서는 추가적인 방문 기록을 로그에 남기게 된다.
제품 환경에서는 중복된 방문 로그가 없을 것이다
보내는 분석 이벤트를 디버깅하려면 앱을 스테이징 환경에 배포하거나 Strinct Mode를 일시적으로 사용 중지하여 개발 환경 전용의 재마운팅 검사를 수행할 수 있다. 또한 Effect 대신 라우트 변경 이벤트 핸들러에서 분석을 보낼 수도 있다.
if(typeof window !== 'undefined') {// 브라우저에서 실행 중인지 확인
checkAuthToken();
loadDataFromLacalStorage();
}
function App() {
}
컴포넌트 외부에서 해당 로직을 실행하면 해당 로직은 브라우저가 페이지를 로드한 후 한 번만 실행됨이 보장된다.
제품을 구매하는 POST 요청을 보내는 Effect가 있다고 가정
useEffect(() => {
fetch('/api/buy', { method: 'POST' });
}, []);
Effect를 삭제하고 /api/buy 요청을 But 버튼의 이벤트 핸들러로 이동
function handleClick() {
fetch('/api/buy', { method: 'POST' });
}
만약 컴포넌트를 다시 마운트했을 때 애플리케이션의 로직이 깨진다면 기존에 존재하던 버그가 드러난 것
import { useState, useEffect } from 'react';
function Playground() {
const [text, setText] = useState('a');
useEffect(() => {
function onTimeout() {
console.log('⏰ ' + text);
}
console.log('🔵 스케줄 로그 "' + text);
const timeoutId = setTimeout(onTimeout, 3000);
return () => {
console.log('🟡 취소 로그 "' + text);
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 ? '마운트 해제' : '마운트'}
</button>
{show && <hr />}
{show && <Playground />}
</>
);
}
처음에는 스케줄 로그 a, 취소 로그 a, 스케줄 로그 a 라는 세 가지 로그가 뜸
몇 초 후에 a 라는 로그가 나타날 것
-> 추가된 스케줄/취소 쌍은 React가 컴포넌트를 개발 중에 한 번 다시 마운트하여 정리를 제대로 구현했는지 확인하기 때문
입력란을 abc로수정:
빠르게 입력하면 스케줄 ab 로그 바로 뒤에 취소 ab 로그와 스케줄 abc 로그가 나타날 것
React는 항상 이전 렌더의 Effect를 다음 렌더의 Effect보다 먼저 정리함
-> 빠르게 입력하더라도 한 번에 최대 하나의 타임아웃만 예약됨
입력란에 무언가 입력한 다음 컴포넌트 마운트 해제를 누르면 마운트 해제가 마지막 렌더의 Effect를 정리함을 볼 수 있다.
-> 타임아웃이 실행되기 전에 마지막 타임아웃이 취소된다.
정리함수를 주석 처리하여 타임아웃이 취소되지 않게 하면:
abcde를 빠르게 입력했을 때 a, ab, abc, abcd, abcde라는 일련의 로그를 볼 수 있음
각 Effect는 해당 렌더의 text 값을 캡처함
text='ab' 렌더의 Effect에서는 항상 ab를 볼 것이다.
-> 각 렌더의 Effect는 서로 격리되어 있음
more... -> 클로저
useEffect를 렌더링 결과물에 부착하는 것으로 생각 가능
export default function ChatRoom({ roomId })[
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => conenctoin.disconnect();
}, [roomId]);
return <h1>welcome to {roomId}</h1>;
}
초기 렌더링
return <h1>Welcome to general</h1>;
Effect 또한 렌더링 결과물의 일부이다.
첫 번째 렌더링의 Effect:
() => {
const connection = createConnection('general');
connection.connect();
return () => connection.disconnect();
},
['general']
React는 이 Effect를 실행하며 general 채팅방에 연결함
같은 의존성 사이에서의 재렌더링
return <h1>Welcome to general</h1>;
React는 렌더링 출력이 변경되지 않았기에 DOM을 업데이트하지 않는다.
두 번째 렌더링에서의 ['general']을 첫 번째 렌더링의 ['general']과 비교
-> 모든 의존성이 동일하므로 React는 두 번째 렌더링에서의 Effectf를 무시함
해당 Effect는 호출되지 않는다.
다른 의존성으로 재렌더링
return <h1>Welcome to travel</h1>;
React는 DOM을 업데이트하여 변경함
세 번째 렌더링에서의 Effect:
() => {
const connection = createConnection('travel');
connection.connect();
return () => connection.disconnect();
}, ['travel']
세 번째 렌더링에서의 ['travel']을 두 번째 렌더링의 ['general']과 비교
React는 세 번째 렌더링의 Effect를 적용하기 전에 먼저 실행된 Effect를 정리해야 한다.
두 번째 렌더링의 Effect는 건너뛰어졌기 때문에 첫 번째 렌더링의 Effect를 정리해야 한다.
처음 렌더링되었을 때 스크롤하면, createConnection('general')로 생성된 연결에 대해 disconnect()를 호출하는 것을 볼 수 있다. -> 앱은 'general' 채팅방과의 연결이 해제된다.
그 후 세 번째 렌더링의 Effect를 실행해 travel 채팅방에 연결한다.
마운트 해제
마지막으로 사용자가 다른 페이지로 이동하게 되어 ChatRoom 컴포넌트가 마운트 해제 된다.
마지막 Effect(세 번째 렌더링)의 클린업 함수를 실행한다.
import { useEffect, useRef } from 'react';
export default function MyInput({ value, onChange }){
const ref = useRef(null);
useEffect(() => {
ref.current.focus();
}, []);
return (
<input
ref={ref}
value={value}
onChange={onChange}
/>
);
}
렌더링 중에 ref.current.focus() 호출은 부적절 -> 부수효과이기 때문
=> useEffect로 선언(특정 상호작용이 아니라 컴포넌트가 나타나는 것에 의해 발생되기 때문)
[] 의존성 -> 렌더링 후 매번 실행되는 것이 아니라 마운트 시에만 실행되도록 함
폼에서 두 개의 MyInput 컴포넌트를 렌더링
form 보기를 누르면 두 번째 필드가 자동으로 포커스됨
-> 두 MyInput 컴포넌트 모두 내부의 필드에 포커스를 주려고 하기 때문에 두 개의 입력 필드에 연속해서 focus를 호출하면 항상 마지막 호출이 승리하게 됨
첫 번째 필드에 포커스를 주려면 첫 번째 MyInput 컴포넌트가 true로 설정된 shouldFocus prop을 받도록 변경해야 함
-> MyInput이 받은 prop이 true일 때만 focus()가 호출되도록 변경
import { useEffect, useRef } from 'react';
export default function MyInput({ shoouldFocus, value, onChange }){
const ref = useRef(null);
useEffect(() => {
if(shouldFocus)
ref.current.focus();
}, [shouldFocus]);
return (
<input
ref={ref}
value={value}
onChange={onChange}
/>
);
}
Counter 컴포넌트 -> 매 초마다 증가하는 카운터
컴포넌트가 마운트될 때 setInterval을 호출
이로 인해 onTick 함수가 매 초마다 실행됨
onTick 함수 -> 카운터를 증가시킴
하지만 1초마다 한 번씩 증가하는 대신 두 번씩 증가함
원인을 찾아 수정하기
힌트 setInterval은 interval ID를 반환한다. 이를 clearInterval 함수에 전달하여 Interval을 중지
import { useEffect, useRef } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
function onTick(){
setCount(c => c + 1);
}
const intervalId = setInterval(onTick, 1000);
return () => clearInterval(intervalId);
}, []);
return <h1>{count}</h1>
}
select 태그로 선택한 사람의 일대기를 보여주는 컴포넌트
-> 선택된 person이 변경될 때마다, 또한 마운트될 때마다 비동기 함수 fetchBio(person) 함수를 호출하여 일대기를 불러옴
이 비동기 함수는 Promise를 반환하며, 이 Promise는 결국 문자열로 resolve됨
불러오기가 완료되면 setBio를 호출하여 해당 문자열을 select의 option으로 표시
import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';
export default function Page() {
const [person, setPerson] = useState('Alice');
const [bio, setBio] = useState(null);
useEffect(() => {
let ignore = false;
setBio(null);
fetchBio(person).then(result => {
if(!ignore) {
setBio(result);
}
});
return () => { ignore = true; }
}, [person]);
return (
<>
<select value={person} onChange={e => {
setPerson(e.target.value);
}}>
<option value="Alice">Alice</option>
<option value="Bob">Bob</option>
<option value="Taylor">Taylor</option>
</select>
<hr />
<p><i>{bio ?? 'Loading...'}</i></p>
</>
);
}
해설
-> Taylor가 선택되어도 Bob의 일대기가 표시된다.
경쟁 조건 발생
클린업 함수가 필요하다
각 렌더링의 Effect는 자체 ignore 변수를 가지고 있다.
처음에 ignore 변수는 false로 설정된다.
Effect가 클린업되면 해당 Effect의 ignore 변수는 true로 설정된다.
마지막 사람의 Effect만 ignore가 false로 설정되어 setBio(result)를 호출한다.