useEffect
는 외부 시스템과 컴포넌트의 싱크를 맞춰주는 훅이에요.
useEffect(setup, dependencies?)
Reference | 레퍼런스
useEffect(setup, dependencies?)
이펙트(Effect)를 선언하기 위해서는 컴포넌트 최상단에서 useEffect
를 선언하세요.
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
// ...
}
Parameters | 파라미터(매개변수)
setup
: 이펙트(Effect)의 로직을 가진 함수. setup 함수는 선택적으로 cleanup 함수도 반환할 수 있어요. 컴포넌트가 DOM에 추가되면 리액트는 setup 함수를 실행해요. 변경된 의존성으로 모든 리렌더링이 진행된 후, 리액트는 먼저 (만약 cleanup 함수가 주어졌다면) 기존의 값으로 cleanup 함수를 실행시켜요. 그리고 나서 새로운 값을 setup 함수를 동작해요. 컴포넌트가 DOM에서 제거된 후, 리액트는 cleanup 함수를 동작할 거예요.dependencies
(선택) :setup
코드 내부에서 참조하는 모든 반응값의 목록. 반응값은 즉시 컴포넌트 바디 안에서 선언된 props, 상태, 그리고 모든 함수와 변수를 포함해요. 만약 linter가 리액트에 맞게 구성되어 있다면, 모든 반응 값이 올바르게 의존성으로 지정되어 있다는 것을 확인해요. 의존성 목록은 일정한 숫자의 아이템을 가지고 있어야하고[dep1, dep2, dep3]
과 같이 인라인으로 작성해야해요. 리액트는 각 의존성을Object.js
의 비교 알고리즘을 사용하여 이전의 값과 비교해요. 만약 이 인자를 생략한다면 해당 이펙트는 컴포넌트의 모든 리렌더링 후에 재실행 될거예요. 의존성 배열을 전달하는 것, 빈 배열을 전달하는 것, 그리고 어떤 배열도 전달하지 않는 것의 차이는 아래의 예시를 참고하세요.
Returns | 반환값
useEffect
는 undefined
를 반환해요.
Caveats | 주의사항
useEffect
는 훅이기 때문에 최상위 컴포넌트 또는 직접 만든 훅에서만 사용할 수 있어요. 반복문이나 조건문 안에서는 호출할 수 없고요. 만약 필요하다면 새로운 컴포넌트로 추출하여 그 안으로 상태를 넣으세요.- 만약 외부 시스템과 동기화를 시도하지 않는다면 이펙트를 사용할 필요가 없어요.
- 엄격한 모드(Strict Mode)가 실행 중이라면 리액트는 첫 번째 실제 setup 실행 이전에 추가적인 개발 전용 setup+cleanup 사이클을 실행해요. 이는 cleanup 로직이 setup 로직을 똑같이 따르고 있는지 그리고 setup 함수가 실행하는 무슨 작업이든 중단하거나 되돌릴 수 있는지를 확인하는 강도 테스트에요. 만약 여기서 문제가 발생한다면 cleanup 함수를 구현하세요.
- 만약 의존성 중 컴포넌트 내부에서 정의된 객체나 함수가 있다면, 이펙트를 필요한 것보다 더 자주 재실행시킬 위험이 있어요. 이를 고치기 위해서는 불필요한 객체나 함수 의존성들을 삭제하세요. 또한 상태 업데이트를 추출하고 반응적이지 않은 로직을 이펙트 밖으로 옮기세요.
- 이펙트가 (클릭과 같은) 상호작용에 의해 발생하지 않는다면 일반적으로 리액트는 이펙트가 작동하기 전에 브라우저가 변경된 화면을 먼저 페인트해요. 만약 이펙트가 (툴팁을 배치하는 것과 같이) 무언가 시각적인 것을 한다면, 그리고 (깜빡거리는 것과 같이) 지연이 눈에 보인다면
useEffect
를useLayoutEffect
로 대체하세요. - 설령 이펙트가 (클릭과 같은) 상호작용에 의해 발생하지 않는다 하더라도 이펙트 안에서 상태 업데이트 과정이 실행되기 전에 브라우저는 화면을 리페인트할 수 있어요. 보통은 이 과정이 당신이 원하는 과정일 거예요. 그러나 만약 브라우저가 화면을 리페인트 하는 것을 막아야만 한다면
useEffect
를useLayoutEffect
로 대체하세요. - 이펙트는 클라이언트 사이드에서만 작동해요. 서버 렌더링 동안은 작동하지 않아요.
Usage | 용법
Connecting to an external system | 외부 시스템과 연결하기
어떤 컴포넌트는 페이지에 표시되는 동안 네트워크, 브라우저 API, 또는 서드 파티 라이브러리와 계속 연결되어 있어야해요. 이러한 시스템은 리액트로 조정되지 않기 때문에 '외부 시스템'이라고 불려요.
컴포넌트를 외부 시스템과 연결하기 위해서는 최상위 컴포넌트에서 useEffect
를 호출하세요.
import { useEffect } from 'react';
import { createConnection } from './chat.js';
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // setup 코드
connection.connect(); // setup 코드
return () => {
connection.disconnect(); // cleanup 코드
};
}, [serverUrl, roomId]); // 의존성 목록
// ...
}
useEffect
에는 두 개의 인자를 넘겨야해요.
- 시스템과 연결하는 setup 코드를 가진 setup 함수 : 시스템과의 연결을 끊는 cleanup 코드를 포함하는 cleanup 함수를 반환해야해요.
- 이러한 함수들 내부에서 사용되는 컴포넌트의 모든 값을 포함한 의존성 목록
리액트는 필요할 때 setup과 cleanup 함수를 호출하기 때문에 여러번 발생할 수 있어요.
- setup 코드는 컴포넌트가 페이지에 추가될 때 동작해요. (마운트)
- 의존성 배열이 변화한 컴포넌트의 모든 리렌더링이 진행된 후에,
- 먼저, 기존의 props와 상태로 cleanup 코드를 실행해요.
- 그 다음, 새로운 props와 상태로 setup 코드를 실행해요.
- cleanup 코드는 컴포넌트가 페이지에서 제거된 후에 최종적으로 한 번만 동작해요. (언마운트)
이 과정을 위의 예시를 통해 설명해볼게요.
위의 ChatRoom
컴포넌트가 페이지에 추가되면, 초기 serverUrl
과 roomId
로 채팅방과 연결할 거예요. 만약 serverUrl
과 roomId
모두가 리렌더링의 결과로 변화한다면(즉, 만약 사용자가 드롭다운에서 다른 채팅방을 선택한다면), 이펙트는 기존의 방과의 연결을 끊고 다음 방과 연결해요. ChatRoom
컴포넌트가 페이지에서 제거될 때, 이펙트는 마지막으로 연결을 끊을 거예요.
버그를 찾는데 도움이 되기 위해, 개발 과정에서 리액트는 setup과 cleanup을 setup 전에 추가적으로 한 번 더 실행해요. 이는 이펙트의 로직이 알맞게 잘 구현되어있는지를 검증하는 강도 테스트(stress-test)에요. 만약 이 과정에서 이슈가 발생한다면, cleanup 함수는 어떤 로직이 잘못된 거예요. cleanup 함수는 setup 함수가 실행하는 모든 기능을 중단하거나 되돌릴 수 있어야해요. 경험상 (프로덕션 과정에서의) 한 번 setup을 불러오는 것과 (개발 과정에서의)setup → cleanup → setup 절차의 차이를 사용자는 구별할 수 없어야만해요. 일반적인 해결책을 참고하세요.
모든 이펙트를 독립적인 프로세스로 작성하고 한 번에 하나의 setup/ceanup 주기를 생각하세요. 컴포넌트가 마운트 과정에 있든, 업데이트 과정에 있든, 언마운트 과정에 있든 중요하지 않아요. cleanup 로직이 setup 로직을 알맞게 미러링한다면 이펙트는 setup과 cleanup을 필요한 한 자주 실행할 수 있게 회복성이 빨라져요.
노트
이펙트는 (채팅 서비스와 같이) 컴포넌트가 외부 시스템과 동기화되도록 만들어줘요. 여기서, 외부 시스템이란 React로 컨트롤 되지 않는 코드를 의미해요. 예를 들면,-
setInterval()
과clearInterval()
로 관리되는 타이머
-window.addEventListener()
과window.removeEventListener()
을 사용한 이벤트 구독
-animation.start()
와animation.reset()
과 같이 API를 가진 서드 파티 애니메이션 라이브러리만약 어떤 외부 시스템과도 연결하지 않는다면 이펙트가 필요하지 않을 거예요.
외부 시스템과의 연결 예시
1. 채팅 서버와 연결하기
이 예시에서 ChatRoom
컴포넌트는 chat.js
에서 정의된 외부 시스템과의 연결을 유지하기 위해 이펙트를 사용해요. ChatRoom
컴포넌트를 보이게 하려면 "채팅 열기"를 누르세요. 이 샌드박스는 개발 모드에서 실행되기 때문에 이곳에 설명한 것처럼 연결을 하고 연결을 끊는 주기를 추가적으로 실행해요. 드롭다운과 인풋을 활용하여 roomId
와 serverUrl
을 변경하고 어떻게 이펙트를 채팅에 다시 연결하는지를 보세요. 마지막으로 이펙트가 연결을 끊는 것을 보기 위해 "채팅 닫기"를 누르세요.
2. 전역 브라우저 이벤트 구독하기
이 예시에서 외부 시스템은 브라우저 DOM 그 자체예요. 일반적으로 이벤트 리스너는 JSX로 지정하지만 전역 window
객체는 이 방법으로 구독할 수 없어요. 이펙트는 window
객체와 연결하고 해당 객체의 이벤트를 구독할 수 있게 만들어줘요. pointermove
이벤트를 구독하는 것은 커서 (또는 손가락)의 위치를 추적하고 빨간 점도 함께 이동하게 만들어요.
3. 애니메이션 트리거하기
이 예시에서 외부 시스템은 animation.js
에 있는 애니메이션 라이브러리에요. 이 라이브러리는 DOM 노드를 인자로 받아 애니메이션을 컨트롤하는 start()
와 stop()
메서드를 노출시키는 FadeInAnimation
라는 자바스크립트 클래스를 제공해요. 이 컴포넌트는 기본 DOM 노드에 접근하기 위해 ref를 사용해요. 이펙트는 ref를 통해 DOM 노드를 읽고 자동적으로 컴포넌트가 보여지면 해당 노드에 맞는 애니메이션을 시작해요.
4. 모달 대화창을 컨트롤하기
이 예시에서 외부 시스템은 브라우저 DOM이에요. ModalDialog
컴포넌트는 <dialog>
를 렌더링해요. 이 컴포넌트는 isOpen
prop를 showModal()
과 close()
메서드 호출과 동기화하기 위해 이펙트를 사용해요.
5. 요소 가시성 추적하기
이 예시에서 외부 시스템은 다시 브라우저 DOM이에요. App
컴포넌트는 거대 목록을 표시하고 Box
컴포넌트를 표시한 후 다른 거대 목록을 표시해요. 목록을 아래로 스크롤하세요. 모든 Box
컴포넌트가 뷰포트에 꽉 차게 보일 때 배경 색이 검은색으로 변하는 것을 확인하세요. 이를 구현하려면 Box
컴포넌트는 IntersectionObserver
를 활용하기 위해 이펙트를 사용해요. 이 브라우저 API는 DOM 요소가 뷰포트 안에서 보이는지를 알려줘요.
Wrapping Effects in custom Hooks | 커스텀 훅 안에서 이펙트 래핑하기
이펙트는 "해치 탈출"이에요. 리액트 외부 단계가 필요할 때나 리액트에 내장된 괜찮은 해결책이 없을 때 사용해요. 만약 수동적으로 이펙트를 자주 작성해야한다면 이는 보통 컴포넌트가 의존하는 공통된 동작을 위한 커스텀 훅을 추출해야한다는 신호에요.
예를 들어, 이 useChatRoom
은 더 선억적인 API 뒤에 이펙트의 로직을 숨기는 커스컴 훅이에요.
function useChatRoom({ serverUrl, roomId }) {
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]);
}
그리고 나면 이처럼 모든 컴포넌트에서 사용할 수 있어요.
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useChatRoom({
roomId: roomId,
serverUrl: serverUrl
});
// ...
리액트 환경에는 모든 목적에 부합하는 많은 커스텀 훅이 있어요.
커스텀 훅에서 이펙트를 감싸는 방법에 대해 더 배우려면 여기를 눌러주세요.
커스텀 훅에서 이펙트를 래핑하는 예시
Controlling a non-React widget | 리액트가 아닌 위젯 컨트롤하기
때때로 외부 시스템을 컴포넌트의 prop이나 상태와 동기화하고 싶을 수 있어요.
예를 들어서 서드 파티 맵 위젯이나 리액트로 작성되지 않은 비디오 플레이어 컴포넌트를 가지고 있다면, 해당 컴포넌트의 상태가 당신의 리액트 컴포넌트의 현재 상태와 일치하도록 만들기 위하여 이펙트를 사용할 수 있어요. 이 이펙트는 map-widget.js
안에 정의된 MapWidget
클래스의 인스턴스를 만들어요. 만약 Map
컴포넌트의 zoomLevel
prop을 수정한다면, 이펙트는 동기화를 유지하기 위해 클래스 인스턴스의 setZoom()
을 호출해요.
이 예시에서 cleanup 함수는 필요하지 않아요. 왜냐하면 MapWidget
클래스는 전달 받은 DOM 노드만을 관리하기 떄문이에요. Map
리액트 컴포넌트가 트리에서 제거된 후, DOM 노드와 MapWidget
클래스 인스턴스 모두 브라우저의 자바스크립트 엔진에 의해 자동으로 쓰레기 수집(가비지 콜렉팅, garbage-collected)이 돼요.
Fetching data with Effect | 이펙트로 데이터 페칭하기
컴포넌트에 데이터를 가져올 때 이펙트를 사용할 수 있어요. 만약 프레임워크를 사용하고 있다면 수동적으로 이펙트를 작성하는 것 보다는 현재 사용하는 프레임워크는 데이터 페칭 메커니즘이 훨씬 더 효율적이라는 사실을 기억하세요.
만약 수동적으로 이펙트를 통해 데이터를 가져오고 싶다면 아래와 같은 방식으로 작성하세요.
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]);
// ...
}
ignore
이라는 변수는 false
로 초기화 되고 cleanup 동안 true
로 정해져요. 이는 코드가 경쟁상태가 되지 않는다는 것을 보장해요. 네트워크 응답은 보낸 순서와 다른 순서로 도착할 거예요.
또한 async
/await
구문을 사용하여 다시 작성할 수 있지만 여전히 cleanup 함수를 받아야해요.
이펙트에서 직접적으로 데이터를 페칭하는 것은 무한반복을 야기하고 캐싱이나 서버 렌더링을 늦추는 것과 같은 최적화 수단을 사용하기 어려워져요. 직접 만들거나 커뮤니티를 통해 제공되는 커스텀 훅을 사용하는 게 더 쉬울 거예요.
더 알아보기
이펙트에서 데이터를 가져오는 것보다 더 나은 대안방안은 무엇일까요?
이펙트 안에 fetch
호출 구문을 작성하는 것은 특히 완벽한 클라이언트 사이드의 어플리케이션일 때, 데이터를 가져올 때 자주 사용되는 방법이에요. 그러나 이는 굉장히 수동적인 접근이며 큰 단점이 존재해요.
- 이펙트는 서버에서 동작하지 않아요. 초기에 서버에서 렌더링된 HTML은 데이터 없이 로딩 상태만을 포함해요. 클라이언트 컴퓨터는 모든 JavaScript를 다운로드하고 어플리케이션을 렌더링해야만 데이터를 로드하는데 필요한지를 알 수 있어요. 이는 굉장히 비효율적이에요.
- 이펙트에서 직접적으로 데이터를 페칭하면 "네트워크 폭포수 효과"가 나타날 수 있어요. 부모 컴포넌트를 렌더링하면, 부모 컴포넌트는 일부 테이터를 가져오고, 자식 컴포넌트를 렌더링한 다음에 그 자식 컴포넌트의 데이터를 가져오기 시작해요. 만약 네트워크가 빠르지 않다면 이는 병렬적으로 모든 데이터를 가져오는 것보다 확실히 느려요.
- 이펙트에서 직접 데이터를 페칭하는 것은 미리 데이터를 로딩하거나 캐싱하지 않는다는 것과 동일한 의미를 가져요. 예를 들어, 만약 컴포넌트가 언마운트 된 후 다시 마운트가 된다면 데이터는 다시 페치될 거예요.
- 인간공학적이지 않아요. 경쟁상태와 같은 버그가 없는 방식으로 fetch
를 호출할 때는 상당한 보일러플레이트 코드가 필요해요.
이러한 단점들은 리액트에만 국한된 것은 아니에요. 어떤 라이브러리에서든 마운트 시 데이터를 페칭한다면 적용되는 단점이에요. 라우팅과 같이, 마운트를 할 때 데이터를 가져오는 것은 쉬운 일이 아니에요. 따라서 우리는 다음과 같은 방법을 추천해요.
- 만약 프레임워크를 사용한다면 해당 프레임워크에 내장되어 있는 데이터 페칭 메커니즘을 사용하세요. 모던 리액트 프레임워크는 효율적이면서 위에 언급된 함정들에 빠지지 않는 데이터 페칭 메커니즘과 통합되어 있어요.
- 다른 방법으로는 클라이언트 사이드 캐싱을 사용하거나 구축하는 것을 고려해보세요. React Query, useSWR 그리고 React Router 6.4+와 같은 오픈 소스 솔루션이 인기가 많아요. 또한 직접 솔루션을 만들 수도 있어요. 이 경우에는 이펙트 안에서 사용하지만 (데이터를 미리 로딩하거나 라우트에 필요한 데이터를 끌어올리는 방법을 통해) 중복된 요청, 응답 캐싱 그리고 네트워크 폭포수 효과 회피 로직을 포함해야해요.
만약 위의 방법 중 맞는 방법이 없다면 이펙트 안에서 데이터 페칭을 직접적으로 진행할 수 있어요.
Specifying reactive dependencies | 반응적인 의존성 지정하기
이펙트의 의존성을 선택할 수 없다는 것을 기억하세요. 이팩트 코드 내부에서 사용되는 모든 반응값은 의존성으로 선언되어야 합니다. 이펙트의 의존성 배열은 아래의 코드처럼 결정할 수 있어요.
function ChatRoom({ roomId }) { // 반응값이에요.
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 이 값도 반응값이에요.
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 이 이펙트는 위의 반응값을 읽어요.
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ 따라서 이 반응값들을 이펙트의 의존성으로 지정해야해요.
// ...
}
만약 serverUrl
이나 roomId
가 바뀐다면 이펙트는 새로운 값으로 채팅을 재연결해요.
반응값은 props와 컴포넌트 내부에서 직접적으로 선언된 모든 변수와 함수를 포함해요. roomId
와 serverUrl
이 반응값이기 때문에 의존성 배열에서 이 두 변수를 제거할 수 없어요. 만약 이 변수들을 삭제하려고 하고 리액트에 맞는 린터가 설정되어 있다면, 린터는 이 부분을 고쳐야하는 실수라고 표시할 거예요.
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 useEffect 훅이 의존성 배열을 놓쳤어요.: roomId와 serverUrl을 추가하세요.
// ...
}
의존성을 지우기 위해서는 의존성을 지정할 필요가 없다는 것을 린터에 "증명"해야해요. 예를 들어 반응값이 아니고 리렌더링마다 바뀌지 않는 다는 것을 증명하기 위하여 serverUrl
을 컴포넌트 외부로 뺄 수 있어요.
const serverUrl = 'https://localhost:1234'; // 더 이상 반응값이 아니에요.
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 모든 의존성이 선언되었어요.
// ...
이제 serverUrl
은 반응값이 아니기 때문에 (그리고 리렌더링 시에도 변하지 않기 때문에) 의존성으로 추가되지 않아도 돼요. 만약 이펙트 코드가 아무 반응값도 사용하지 않는다면 의존성 배열은 빈배열([]
)이 될 수 있어요.
const serverUrl = 'https://localhost:1234'; // 더 이상 반응값이 아니에요.
const roomId = 'music'; // 더 이상 반응값이 아니에요.
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ 모든 의존성이 선언되었어요.
// ...
}
빈 배열을 의존성 배열로 가진 이펙트는 이펙트 컴포넌트의 어떤 props나 어떤 상태가 변화해도 다시 실행되지 않아요.
함정
만약 이미 베이스 코드가 존재한다면, 아래와 같이 린터를 억제하는 이펙트를 가지고 있을 수도 있어요.
useEffect(() => { // ... // 🔴아래와 같이 린터를 억제하는 것을 피하세요. // eslint-ignore-next-line react-hooks/exhaustive-deps }, []);
의존성 배열이 코드와 맞지 않을 때, 버그를 발생시킬 위험성이 높아요. 린터를 억제하여 이펙트가 의존하고 있는 값을 리액트에게 "속이고" 있는 거예요. 대신, 이 값들이 불필요하다는 것을 증명하세요.
의존성 배열을 넘기는 예시
1. 의존성 배열 넘기기
만약 의존성 배열을 지정한다면 이펙트는 최초 렌더링 후에, 그리고 배열 안에 있는 의존성이 변경되는 리렌더링 후에 작동해요.
useEffect(() => {
// ...
}, [a, b]); // a 또는 b의 값이 변할 때 재실행 돼요.
아래의 예시에서 serverUrl
와 roomId
는 반응값이고 둘 다 반드시 의존성으로 지정되어야해요. 결론적으로 드롭다운에서 다른 방을 고르거나 서버 URL 인풋을 수정하는 것은 채팅이 재연결되도록 만들어요. 그러나 message
는 이펙트에서 사용되지 않기 때문에 (그리고 의존성이 아니기 때문에) 메시지를 수정하는 것은 채팅에 재연결시키지는 않아요.
2. 빈 의존성 배열 넘기기
만약 이펙트가 정말로 어떤 반응값도 필요로하지 않는다면, 최초 렌더링 후에만 작동해요.
useEffect(() => {
// ...
}, []); // (개발 과정을 제외하고) 다시 작동하지 않아요
빈 의존성 배열이더라도 setup와 cleanup은 버그를 찾기 위하여 개발 과정에서는 추가적으로 한 번 더 실행해요.
이 예시에서는 serverUrl
과 roomId
가 모두 하드코딩이 되어있어요. 두 변수가 컴포넌트 외부에서 선언되어 있어서 반응값이 아니고 의존성 변수가 아니에요. 의존성 목록은 비어있고 이펙트는 리렌더링이 될 때 다시 작동하지 않아요.
3. 아무것도 넘기지 않기
만약 어떤 배열도 넘기지 않는다면 이펙트는 컴포넌트의 모든 렌더링과 리렌더링마다 실행될 거예요.
useEffect(() => {
// ...
}); // 항상 재실행해요.
이 예시에서 이펙트는 serverUrl
과 roomId
가 변화할 때 실행되고, 합리적이에요. 그러나 messgae
를 바꿀 때도 재실행돼요. 그리고 이는 아마 원치 않을 거예요. 이는 보통 의존성 배열을 지정해야하는 이유에요.
Updating state based on previous state from an Effect | 이펙트의 이전 상태를 바탕으로 상태 업데이트하기
이펙트의 이전 상태를 바탕으로 상태를 업데이트 하려면 아래와 같은 문제와 마주할 거예요.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // 매 초마다 count를 1씩 증가시키고 싶어요.
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... 하지만 `count`를 의존성으로 지정하면 항상 간격이 재설정 되어서 useEffect가 반복적으로 실행돼요.
// ...
}
count
는 반응값이기 때문에 의존성 배열에 지정이 되어있어야 해요. 하지만 이는 이펙트가 cleanup과 setup을 count
가 바뀔 때마다 재실행 하도록 만들어요. 그리고 이는 이상적이지 않아요.
이 문제를 해결하려면 setCount
에 c => c + 1
라는 상태 업데이터를 전달해야해요.
이제 c => c + 1
을 count + 1
대신 전달했기 때문에 이 이펙트는 더 이상 count
에 의존할 필요가 없어요. 이 해결방법의 결론은 count
가 바뀔 때마다 간격을 재설정하여 cleanup과 setup 함수를 실행할 필요가 없어요.
Removing unnecessary object dependencies | 불필요한 객체 의존성 제거하기
만약 이펙트가 렌더링 동안 생성된 객체나 함수에 의존한다면, 이펙트는 너무 자주 실행돼요. 예를 들어, 아래의 이펙트는 options
객체가 매 렌더링 마다 변하기 때문에 매 렌더링 후에 재연결돼요.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 이 객체는 매 렌더링마다 스크래치로부터 생성돼요.
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 결론적으로, 이러한 의존성들은 리렌더링마다 달라져요.
// ...
}
렌더링 동안 생성되는 객체를 의존성으로 사용하는 것을 피하세요. 대신 이펙트 내부에서 객체를 생성하세요.
이제 이펙트 내부에서 options
객체를 생성하고 이펙트는 roomId
문자열에만 의존해요.
이렇게 고치면, 인풋창 안에 타이핑을 하는 것은 채팅을 재연결하지는 않아요. 재생성되는 객체와는 달리 roomId
와 같은 문자열은 또 다른 값을 해당 변수에 할당하지 않으면 변하지 않아요. 의존성 제거에 대한 더 많은 정보를 알아보세요.
Removing unnecessary function dependencies | 불필요한 함수 의존성 제거하기
만약 이펙트가 렌더링 동안 생성된 객체나 함수에 의존한다면, 이펙트는 너무 자주 실행돼요. 예를 들어, 아래의 이펙트는 createOptions
함수가 매 렌더링 마다 변하기 때문에 매 렌더링 후에 재연결돼요.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 이 함수는 매 렌더링마다 스크래치로부터 생성돼요.
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 결론적으로, 이러한 의존성들은 리렌더링마다 달라져요.
// ...
매 렌더링마다 스크래치로부터 함수가 생성되는 것 자체만으로는 문제는 아니에요. 이를 최적화할 필요는 없어요. 그러나, 만약 이런 함수를 이펙트의 의존성으로 사용한다면 매 리렌더링 후에 이펙트도 재실행시킬 거예요.
렌더링 동안 생성되는 함수를 의존성으로 사용하는 것을 피하세요. 대신 이펙트 내부에서 함수를 생성하세요.
이제 이펙트 내부에서 createOptions
함수를 정의했고 이펙트는 roomId
문자열에만 의존해요. 이렇게 고치면, 인풋창 안에 타이핑을 하는 것은 채팅을 재연결하지는 않아요. 재생성되는 객체와는 달리 roomId
와 같은 문자열은 또 다른 값을 해당 변수에 할당하지 않으면 변하지 않아요. 의존성 제거에 대한 더 많은 정보를 알아보세요.
Reading the latest props and state from an Effect | 이펙트에서 가장 최신의 props와 상태 읽기
공사 중
이 부분은 리액트의 안정적인 버전에서는 아직 릴리즈 되지 않은 실험적인 API를 설명하고 있어요.
기본적으로 이펙트에서 반응값을 읽으려면 이 값을 의존성에 넣어야해요. 이는 이펙트가 해당 값의 모든 변화에 "반응한다"는 것을 보장해요. 대부분의 의존성에서는 원하는 행동이 바로 이 행동일 거예요.
그러나 때때로 props나 상태에 "반응"하지 않고 이펙트에서 최근의 props와 상태를 읽어오고 싶을 거예요. 예를 들어, 방문한 모든 페이지에서 장바구니에 있는 상품의 수를 기록하고 싶다고 생각해보아요.
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ 모든 의존성을 선언했어요.
// ...
}
만약 shoppingcart
의 변화를 제외하고 모든 url
이 변한 후에는 새로운 페이지를 기록하고 싶다면 어떨까요? 반응성 규칙을 깨뜨리지 않은 채로 shoppingCart
를 의존성에서 제외할 수 없어요. 그러나 설령 이펙트 내부에서 호출된 코드라고 하더라도 변화에 반응하지 않고 싶을 수도 있어요. useEffectEvent
훅을 사용하여 이펙트 이벤트를 선언하고 그 안에서 shoppingCart
를 읽는 코드를 옮기세요.
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ 모든 의존성을 선언했어요.
// ...
}
이펙트 이벤트는 반응적이지 않고 이펙트의 의존성에서 빠져야만 해요. 이벤트 내부에 (몇몇 props와 상태의 가장 최근 값을 읽어오려는) 반응적이지 않은 코드를 배치하는 것이에요. onVisit
의 내부에서 shoppingCart
를 읽음으로서 shoppingcart
가 이펙트를 재실행 시킨다는 것을 보장해요.
이펙트 이벤트가 반응적인 코드와 비반응적인 코드를 어떻게 분리하는지 알아보세요.
Displaying different content on the server and the client | 서버와 클라이언트에서 서로 다른 콘텐츠를 표시하세요.
만약 어플리케이션이 (직접적으로든 프레임워크를 통해서든) 서버 렌더링이 필요하다면 컴포넌트는 두 개의 다른 환경 위에서 렌더링될 거예요. 서버에서는 최초의 HTML을 생성하기 위해 렌더링을 할 거예요. 클라이언드에서는 리액트가 다시 코드 렌더링을 시작하기 때문에 HTML에서 이벤트 엔들러에 접근할 수 있어요.
드문 경우로, 클라이언트에서 다른 콘텐츠를 표시할 필요가 있을지도 몰라요. 예를 들어서 어플리케이션이 localStorage
에서 어떤 데이터를 읽는다면, 아마도 서버에서는 할 수 없을 거예요. 어떻게 구현하는지는 아래를 확인하세요.
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... 클라이언트에서만 실행가능 한 JSX를 반환해요 ...
} else {
// ... 초기 JSX를 반환해요 ...
}
}
앱이 로딩되는 동안 사용자는 최근 렌더링된 결과물을 볼 거예요. 그리고나서 리렌더링을 트리깅하는 로딩이 되고 하이드레이트가 되면 이펙트는 실행되고 didMount
가 true가 되며 렌더링이 트리거가 돼요. 이펙트는 서버에서 실행되지 않으며 이는 didMount
가 초기 서버 렌더링동안 false
였던 이유에요.
이 패턴은 사용을 삼가세요. 명심하세요. 연결 속도가 느린 사용자는 꽤 오랜시간 동안(어쩌면 몇 초간은) 초기 콘텐츠를 보게 되기 때문에 컴포넌트의 외형에 급격한 변화를 주면 안돼요. 많은 경우에서는 CSS를 사용하여 조건적으로 다른 콘텐츠를 보여주는 방법을 통하여 이러한 방법을 사용하는 것을 피할 수 있어요.
Troubleshooting | 트러블슈팅
My Effect runs twice when the components mounts | 컴포넌트가 마운트 될 때 이펙트가 2번 실행돼요.
엄격한 모드(Strict Mode)가 켜져있으면 개발 과정에서 리액트는 setup와 cleanup을 실제 setup을 실행하기 이전에 추가적으로 한 번 더 실행해요.
이는 이펙트의 로직이 알맞게 잘 구현되어있는지를 검증하는 강도 테스트(stress-test)에요. 만약 이 과정에서 이슈가 발생한다면, cleanup 함수는 어떤 로직이 잘못된 거예요. cleanup 함수는 setup 함수가 실행하는 모든 기능을 중단하거나 되돌릴 수 있어야해요. 경험상 (프로덕션 과정에서의) 한 번 setup을 불러오는 것과 (개발 과정에서의)setup → cleanup → setup 절차의 차이를 사용자는 구별할 수 없어야만해요.
이 과정이 어떻게 버그를 찾는 것을 돕는지와 로직을 어떻게 고쳐야하는지를 더 알아보세요.
My Effect run after every re-render | 매 리렌더링 후에 이펙트가 작동해요.
먼저, 의존성 배열을 지정하는 것을 잊지 않았는지 확인하세요.
useEffect(() => {
// ...
}); // 🚩 의존성 배열이 없어요: 모든 렌더링 후에 재실행 돼요.
만약 의존성 배열은 지정했지만 여전히 반복된다면 의존성 중 하나가 모든 리렌더링마다 달라진다는 것이에요.
이 문제는 수동적으로 콘솔에 의존성에 기록을 찍어보면 디버깅 할 수 있어요.
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
그 다음에 콘솔에서 다른 리렌더링에서 나온 서로 다른 배열을 오른쪽 마우스 클릭을 하고 " 전역 변수로 저장"을 두 배열 모두에서 선택하세요. 첫 번째는 temp1
로 저장되고 두번째는 temp2
로 저장된다고 가정하면 브라우저 콘솔에서 각 배열에 안에 들어간 의존성이 동일한지를 확인할 수 있어요.
Object.is(temp1[0], temp2[0]); // 첫 번째 배열이 배열 간에 동일한가요?
Object.is(temp1[1], temp2[1]); // 두 번째 배열이 배열 간에 동일한가요?
Object.is(temp1[2], temp2[2]); // ... 그리고 모든 의존성에서도 동일한지 확인하기 ...
매 리렌더링마다 의존성을 찾는다면 보통은 아래의 방법 중 하나로 해결할 수 있어요.
이펙트의 이전 상태를 바탕으로 상태 업데이트하기
불필요한 객체 의존성 제거하기
불필요한 함수 의존성 제거하기
이펙트에서 가장 최신의 props와 상태 읽기
(만약 위의 방법들이 도움이 되지 않았다면) 최후의 수단으로 useMemo
나 (함수의 경우) useCallback
을 생성할 때 감싸세요.
My Effect keeps re-running in an infinite cycle | 이펙트가 무한 사이클로 계속 재실행돼요.
만약 이펙트가 무한 사이클로 실행된다면 아래의 두가지를 모두 만족하고 있는 거예요.
- 이펙트는 상태를 업데이트하고 있어요.
- 그 상태는 리렌더를 이끌고 이는 이펙트의 의존성이 변경되는 것을 야기해요.
문제 해결을 시작하기 전에 이펙트가 (DOM, 네트워크, 서드 파티 위젯 등등과 같은) 외부 시스템과 연결되어 있는지를 스스로 물어보세요. 이펙트가 상태를 설정해야하는 이유가 있나요? 외부 시스템과 상태가 동기화가 되었나요? 또는 그 상태로 어플리케이션의 데이터 흐름을 관리할 수 있나요?
만약 외부 시스템이 없다면 이펙트를 제거했을 때 로직이 간단해지는지 고민해보세요.
만약 정말로 일부 외부 시스템과 동기화하고 있다면 왜그런지 그리고 어떤 조건에서 이펙트가 상태를 업데이트하는지를 생각해보세요. 컴포넌트의 시각적 결과물에 영향을 미칠 수 있는 어떤 변화가 있나요? 만약 렌더링에 사용되지 않는 데이터를 계속 추적해야한다면 (리렌더링을 트리거하지 않는) ref가 더 적절할 수 있어요. 이펙트가 필요한 것보다 더 상태를 업데이트하지는 않는지 (그리고 리렌더링을 트리거하지 않는지)를 검사하세요.
마지막으로 이펙트가 알맞은 때에 상태를 업데이트 한다면, 하지만 여전히 반복된다면, 상태 업데이트가 이펙트의 의존성 중 하나를 바꾸기 때문이에요. 의존성 변경을 어떻게 디버깅하는지 읽어보세요.
My cleanup logic runs even though my component didin't unmount | cleanup 로직이 컴포넌트가 해제되지 않아도 동작해요.
cleanup 함수는 오직 언마운트 중에만 실행될 뿐만 아니라 의존성이 변경된 모든 리렌더링 이전에도 실행돼요. 더해서, 개발 단계에서 리액트는 setup+cleanup을 컴포넌트가 마운트 된 이후에 즉시 한 번 더 실행해요.
만약 setup 코드와 상응하지 않는 cleanup 코드가 있다면 보통은 코드에 문제가 있는 거예요.
useEffect(() => {
// 🔴 setup 로직와 상응하지 않는 cleanup 로직은 피하세요.
return () => {
doSomething();
};
}, []);
cleanup 로직은 setup 로직에 대칭적이어야하고 setup이 했던 무슨 작업이든 멈추거나 되돌릴 수 있어야해요.
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
이펙트의 라이프사이클이 컴포넌트의 라이프사이클과 어떻게 다른지 알아보세요.
My Effect does something visual, and I see flicker before it runs | 이펙트가 시각적인 무언가를 하고, 이펙트가 작동하기 전에 화면이 깜빡거려요.
만약 이펙트가 브라우저를 화면을 칠하지 못하게 막아야만 한다면 useEffect
를 useLayoutEffect
로 대체하세요. 이것은 대다수의 이펙트에는 필요치 않다는 사실을 명심하세요. 브라우저가 페인트 되기 전에 이펙트를 실행하는 것이 중요할 때만 이기능이 필요할 거예요. 예를 들어 유저가 보기 전에 툴팁을 측정하고 배치하는 것과 같은 일에서요.
'리액트 공식문서 | React Docs > Reference > react@18.2.0' 카테고리의 다른 글
[Hooks] useImperativeHandle | useImperativeHandle훅 (1) | 2024.01.21 |
---|---|
[Hooks] useId | useId 훅 (0) | 2024.01.21 |
[Hooks] useDeferredValue | useDeferredValue 훅 (1) | 2024.01.14 |
[Hooks] useDebugValue | useDebugValue 훅 (0) | 2024.01.11 |
[Hooks] useContext | useContext 훅 (2) | 2024.01.07 |