
참고 : https://react.dev/reference/react/useEffect
useEffect는 외부 시스템과 컴포넌트를 동기화할 수 있는 React Hook입니다.
useEffect(setup, dependencies?)
컴포넌트의 top level에 있는 useEffect를 호출하여 Effect를 정의할 수 있습니다:
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]);
// ...
}
Object.is 비교를 사용하여 각 종속성을 이전 값과 비교합니다. 종석성 배열을 전달하는 것, 빈 배열을 전달하는 것, 종속성이 전혀 없는 것의 차이를 아래에서 확인해보세요. useEffect는 undefined를 리턴합니다.
일부 컴포넌트는 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 계속 연결되어 있어야 합니다. 이러한 시스템은 React에 의해 제어되지 않으므로 외부 시스템이라고 합니다.
일부 외부 시스템에 컴포넌트를 연결하려면 컴포넌트의 최상위 수준에서 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]);
// ...
}
Effect를 사용하려면 두 개의 arguments를 통과해야 합니다:
리액트는 필요할 때마다 setup 및 cleanup 함수를 호출하며, 여러 번 발생할 수 있습니다:
위의 예를 위해 이 순서를 설명해 보자.
위의 ChatRoom 컴포넌트가 페이지에 추가되면 초기 serverUrl 및 roomId를 사용하여 채팅방에 연결됩니다. 리렌더링의 결과로 serverUrl 또는 roomId가 변경되면(예: 사용자가 드롭다운에서 다른 채팅방을 선택하는 경우) Effect는 이전 방과의 연결이 끊어지고 다음 방에 연결됩니다. ChatRoom 컴포넌트가 페이지에서 제거되면 마지막으로 Effect의 연결이 끊어집니다.
버그를 찾는 데 도움을 주기 위해 개발 단계에서 React는 setup 전에 한 번 더 setup 및 cleanup를 실행합니다. 이는 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트입니다. 이로 인해 눈에 띄는 문제가 발생하면 cleanup 함수에 일부 논리가 누락된 것입니다. cleanup 기능은 setup 기능이 수행하던 모든 작업을 중지하거나 실행 취소해야 합니다. 경험상 사용자는 한 번 호출되는 설정(프로덕션에서와 같이)과 setup → cleanup → setup 순서(개발에서와 같이)를 구별할 수 없어야 합니다. 일반적인 솔루션을 확인하세요.
모든 Effect를 독립적인 프로세스로 작성하고 한 번에 단일 setup/cleanup 주기를 생각해 보십시오. 구성 요소가 마운트, 업데이트 또는 마운트 해제 중인지는 중요하지 않습니다. cleanup 로직이 setup 로직을 올바르게 "미러링"하면 Effect는 필요한 만큼 자주 setup 및 cleanup를 실행하는 데 탄력성을 갖습니다.
Effects는 "탈출구(Escape Hatches)"입니다. "React 외부로 나가야" 할 때나 사용 사례에 더 나은 내장 솔루션이 없을 때 효과를 사용합니다. Effect를 수동으로 작성해야 하는 경우가 많다면 이는 일반적으로 컴포넌트가 의존하는 일반적인 동작에 대한 custom Hooks을 추출해야 한다는 신호입니다.
예를 들어, useChatRoom custom Hook을 사용하면 효과의 논리를 보다 선언적인 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
});
// ...
리액트 생태계에서 사용 가능한 모든 용도의 훌륭한 custom Hooks도 많이 있습니다. custom Hooks에서 Effects 랩핑에 대해 자세히 알아봅니다. ("Reusing Logic with Custom Hooks")
때로는 외부 시스템을 컴포넌트의 특정 상태에 동기화하려는 경우도 있습니다.
예를 들어 React 없이 작성된 타사 지도 위젯이나 비디오 플레이어 구성 요소가 있는 경우 Effect를 사용하여 해당 상태를 React 구성 요소의 현재 상태와 일치시키는 메서드를 호출할 수 있습니다. 이 Effect는 map-widget.js에 정의된 MapWidget 클래스의 인스턴스를 생성합니다. Map 컴포넌트의 zoomLevel prop을 변경하면 Effect는 클래스 인스턴스에서 setZoom()을 호출하여 동기화를 유지합니다.
import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';
export default function Map({ zoomLevel }) {
const containerRef = useRef(null);
const mapRef = useRef(null);
useEffect(() => {
if (mapRef.current === null) {
mapRef.current = new MapWidget(containerRef.current);
}
const map = mapRef.current;
map.setZoom(zoomLevel);
}, [zoomLevel]);
return (
<div
style={{ width: 200, height: 200 }}
ref={containerRef}
/>
);
}

이 예에서는 MapWidget 클래스가 전달된 DOM 노드만 관리하므로 cleanup 함수가 필요하지 않습니다. Map React 구성 요소가 트리에서 제거된 후 DOM 노드와 MapWidget 클래스 인스턴스는 모두 브라우저 JavaScript 엔진에 의해 자동으로 가비지 수집됩니다.
Effect를 사용하여 컴포넌트에 대한 데이터를 가져올 수 있습니다. 프레임워크(Next.js, Remix 등)를 사용하는 경우 프레임워크의 데이터 가져오기 메커니즘을 사용하는 것이 Effect를 수동으로 작성하는 것보다 훨씬 더 효율적이라는 점에 유의하세요.
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]);
// ...
false로 초기화되고 cleanup 중에 true로 설정되는 ignore 변수에 유의하십시오. 이렇게 하면 코드에 "race conditions"이 발생하지 않습니다. "race conditions"는 네트워크 응답은 보낸 순서와 다른 순서로 도착할 수 있습니다.
async / await 구문을 사용하여 다시 쓸 수도 있지만 cleanup 함수를 제공해야 합니다:
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(() => {
async function startFetching() {
setBio(null);
const result = await fetchBio(person);
if (!ignore) {
setBio(result);
}
}
let ignore = false;
startFetching();
return () => {
ignore = true;
}
}, [person]);
Effects에서 데이터 가져오기를 직접 작성하는 것은 반복적이며 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기 어렵게 만듭니다. 자체적으로 사용하거나 커뮤니티에서 관리하는 custom Hook을 사용하는 것이 더 쉽습니다.
Q. Effects에서 데이터 가져오기를 대체할 수 있는 좋은 대안은 무엇입니까? (상당히 좋은 내용이네요. 👏👏👏)
Effects 내에서 가져오기 호출을 작성하는 것은 특히 완전한 클라이언트 측 앱에서 데이터를 가져오는 데 널리 사용되는 방법입니다. 그러나 이는 매우 수동적인 접근 방식이며 상당한 단점이 있습니다.
이 단점 목록은 React에만 국한된 것이 아닙니다. 모든 라이브러리를 사용하여 마운트할 때 데이터를 가져오는 데 적용됩니다. 라우팅과 마찬가지로 데이터 가져오기도 잘 수행하기가 쉽지 않으므로 다음 접근 방식을 권장합니다.
Effect의 종속성을 "선택"할 수 없다는 점에 유의하세요. Effect의 코드에서 사용되는 모든 반응 값은 종속성으로 선언되어야 합니다. Effect의 종속성 목록은 주변 코드에 따라 결정됩니다.
function ChatRoom({ roomId }) { // This is a reactive value
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
// ...
}
serverUrl 또는 roomId가 변경되면 Effect는 새 값을 사용하여 채팅에 다시 연결됩니다.
반응형 값에는 props와 컴포넌트 내부에 직접 선언된 모든 변수 및 함수가 포함됩니다. roomId 및 serverUrl은 반응형 값이므로 종속성에서 제거할 수 없습니다. 이를 생략하려고 시도하고 linter가 React에 대해 올바르게 구성되었다면, linter는 이를 수정해야 할 실수로 표시합니다.
function ChatRoom({ roomId }) {
const [serverUrl, setServerUrl] = useState('https://localhost:1234');
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
// ...
}
종속성을 제거하려면 종속성일 필요가 없다는 것을 Linter에 "증명"해야 합니다. 예를 들어, serverUrl을 컴포넌트 밖으로 이동하여 반응형이 아니며 다시 렌더링 시 변경되지 않음을 증명할 수 있습니다.
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
이제 serverUrl은 반응형 값이 아니므로(다시 렌더링할 때 변경할 수 없음) 종속성일 필요가 없습니다. Effect의 코드가 반응 값을 사용하지 않는 경우 해당 종속성 목록은 비어 있어야 합니다([]).
const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}
빈 종속성이 있는 Effect는 컴포넌트의 props이나 state가 변경될 때 다시 실행되지 않습니다.
Effect에서 이전 상태를 기준으로 상태를 업데이트하려는 경우 다음과 같은 문제가 발생할 수 있습니다:
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // You want to increment the counter every second...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
// ...
}
count는 반응형 값이므로 종속성 목록에 지정되어야 합니다. 그러나 이로 인해 count가 변경될 때마다 Effect가 cleanup 되고 다시 setup 됩니다. 이는 이상적이지 않습니다. (아니.. 뭐 count가 종속성 목록에 있어도 동작은 한다. 근데 이상적이진 않다는 거지)
이 문제를 해결하려면 c => c + 1 상태 업데이트를 setCount에 전달하세요.
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1); // ✅ Pass a state updater
}, 1000);
return () => clearInterval(intervalId);
}, []); // ✅ Now count is not a dependency
return <h1>{count}</h1>;
}
이제 count + 1 대신 c => c + 1을 전달하므로 Effect는 더 이상 count에 의존할 필요가 없습니다. 이 수정으로 인해 count가 변경될 때마다 interval을 다시 정리하고 설정할 필요가 없습니다.
Effect가 렌더링 중에 생성된 객체나 함수에 의존하는 경우 너무 자주 실행될 수 있습니다. 예를 들어, options 객체가 매 렌더링마다 다르기 때문에 Effect는 렌더링할 때마다 다시 연결됩니다.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 This object is created from scratch on every re-render
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // It's used inside the Effect
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
렌더링 중에 생성된 객체를 종속성으로 사용하지 마세요. 대신 Effect 내부에 개체를 만듭니다.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
const options = {
serverUrl: serverUrl,
roomId: roomId
};
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return (
<>
<h1>Welcome to the {roomId} room!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
</>
);
}
이제 Effect 내부에 options 객체를 만들었으므로 Effect 자체는 roomId 문자열에만 의존합니다.
이번 수정으로 인해 input 내용을 입력해도 채팅이 다시 연결되지 않습니다. 다시 생성되는 객체와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다. 종속성 제거에 대해 자세히 알아보세요.
이번에는 불필요한 함수에 대해 종속성이 설정된 경우를 살펴보겠습니다. 예를 들어 이 Effect는 렌더링마다 createOptions 함수가 다르기 때문에 렌더링할 때마다 다시 연결됩니다.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
function createOptions() { // 🚩 This function is created from scratch on every re-render
return {
serverUrl: serverUrl,
roomId: roomId
};
}
useEffect(() => {
const options = createOptions(); // It's used inside the Effect
const connection = createConnection();
connection.connect();
return () => connection.disconnect();
}, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
// ...
그 자체로는 다시 렌더링할 때마다 처음부터 함수를 만드는 것은 문제가 되지 않습니다. 이를 최적화할 필요는 없습니다. 그러나 이를 Effect의 종속성으로 사용하면 다시 렌더링할 때마다 Effect가 다시 실행됩니다.
렌더링 중에 생성된 함수를 종속성으로 사용하지 마세요. 대신 Effect 내부에 선언하세요.
import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
useEffect(() => {
function createOptions() {
return {
serverUrl: serverUrl,
roomId: roomId
};
}
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
❗️ 본 절에서는 아직 안정적인 버전의 React로 공개되지 않은 실험용 API에 대해 설명합니다.
기본적으로 Effect에서 반응 값을 읽을 때 이를 종속성으로 추가해야 합니다. 이렇게 하면 Effect가 해당 값의 모든 변경에 "반응"하게 됩니다. 대부분의 종속성에서는 이것이 원하는 동작입니다.
그러나 때로는 "반응"하지 않고 Effect의 최신 props 및 상태를 읽고 싶을 수도 있습니다. 예를 들어, 페이지를 방문할 때마다 장바구니에 담긴 항목 수를 기록한다고 가정해 보겠습니다.
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
url이 변경될 때마다 새 페이지 방문을 기록하고 싶지만 shoppingCart만 변경된 경우는 기록하지 않으려면 어떻게 해야 합니까? 반응성 규칙을 위반하지 않고는 shoppingCart를 종속성에서 제외할 수 없습니다. 그러나 Effect 내부에서 호출되더라도 코드 조각이 변경 사항에 "반응"하는 것을 원하지 않는다고 표현할 수 있습니다. useEffectEvent Hook를 사용하여 Effect Event를 선언하고 그 안에 shoppingCart를 읽는 코드를 이동합니다.
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
Effect Events는 반응적이지 않으며 항상 Effect의 종속성에서 생략되어야 합니다. 이것이 비반응형 코드(일부 props 및 상태의 최신 값을 읽을 수 있는 코드)를 그 안에 넣을 수 있게 해주는 것입니다. onVisit 내부에서 shoppingCart를 읽으면 shoppingCart가 Effect를 다시 실행하지 않도록 할 수 있습니다.
앱이 서버 렌더링을 사용하는 경우(직접 또는 프레임워크를 통해) 컴포넌트는 두 가지 다른 환경에서 렌더링됩니다. 서버에서는 초기 HTML을 생성하기 위해 렌더링됩니다. 클라이언트에서 React는 이벤트 핸들러를 해당 HTML에 연결할 수 있도록 렌더링 코드를 다시 실행합니다. 이것이 바로 하이드레이션이 작동하려면 클라이언트와 서버에서 초기 렌더링 출력이 동일해야 하는 이유입니다.
드문 경우지만 클라이언트에 다른 콘텐츠를 표시해야 할 수도 있습니다. 예를 들어 앱이 localStorage에서 일부 데이터를 읽는 경우 서버에서는 해당 작업을 수행할 수 없습니다. 이를 구현하는 방법은 다음과 같습니다.
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}
앱이 로드되는 동안 사용자는 초기 렌더링 출력을 볼 수 있습니다. 그런 다음 로드되고 hydrated 되면 Effect가 실행되고 didMount를 true로 설정하여 다시 렌더링을 트리거합니다. 그러면 클라이언트 전용 렌더링 출력으로 전환됩니다. Effect는 서버에서 실행되지 않으므로 초기 서버 렌더링 중에 didMount가 false인 이유입니다.
이 패턴을 아껴서 사용하세요. 연결 속도가 느린 사용자는 꽤 오랜 시간(잠재적으로 몇 초) 동안 초기 콘텐츠를 보게 되므로 컴포넌트의 모양을 부자연스럽게 변경하는 것을 원하지 않는다는 점을 명심하세요. 대부분의 경우 CSS를 사용하여 조건부로 다양한 항목을 표시하면 이러한 필요성을 피할 수 있습니다.
Strict Mode가 켜져 있으면 개발 중 React는 실제 setup 전에 setup 및 cleanup를 한 번 더 실행합니다.
이는 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트입니다. 이로 인해 눈에 띄는 문제가 발생하면 cleanup 함수에 일부 논리가 누락된 것입니다. cleanup 함수는 setup 함수가 수행하던 모든 작업을 중지하거나 실행 취소해야 합니다. 경험상 사용자는 한 번 호출되는 설정(프로덕션에서와 같이)과 설정 → 정리 → 설정 순서(개발에서와 같이)를 구별할 수 없어야 합니다.
먼저 종속성 배열을 지정하는 것을 잊지 않았는지 확인하세요.
useEffect(() => {
// ...
}); // 🚩 No dependency array: re-runs after every render!
종속성 배열을 지정했지만 Effect가 여전히 루프에서 다시 실행된다면 이는 다시 렌더링할 때마다 종속성 중 하나가 다르기 때문입니다.
콘솔에 종속성을 수동으로 기록하여 이 문제를 디버깅할 수 있습니다.
useEffect(() => {
// ..
}, [serverUrl, roomId]);
console.log([serverUrl, roomId]);
그런 다음 콘솔에서 다른 리렌더링의 배열을 마우스 오른쪽 버튼으로 클릭하고 두 항목 모두에 대해 "전역 변수로 저장"을 선택할 수 있습니다. 첫 번째 항목이 temp1로 저장되고 두 번째 항목이 temp2로 저장되었다고 가정하면 브라우저 콘솔을 사용하여 두 배열의 각 종속성이 동일한지 확인할 수 있습니다.
Object.is(temp1[0], temp2[0]); // Is the first dependency the same between the arrays?
Object.is(temp1[1], temp2[1]); // Is the second dependency the same between the arrays?
Object.is(temp1[2], temp2[2]); // ... and so on for every dependency ...
모든 재렌더마다 다른 종속성을 발견하면 대개 다음 방법 중 하나로 수정할 수 있습니다:
마지막 수단으로 (이러한 방법이 도움이 되지 않을 경우) useMemo 또는 useCallback(함수의 경우)으로 랩핑합니다.
Effect가 무한 순환으로 실행되는 경우 다음 두 가지 사항이 참이어야 합니다:
문제 해결을 시작하기 전에 Effect가 일부 외부 시스템(예: DOM, 네트워크, 타사 위젯 등)에 연결되어 있는지 자문해 보세요. Effect에서 상태를 설정해야 하는 이유는 무엇입니까? 해당 외부 시스템과 동기화됩니까? 아니면 이를 통해 애플리케이션의 데이터 흐름을 관리하려고 하시나요?
외부 시스템이 없는 경우 Effect를 완전히 제거하면 논리가 단순화되는지 고려하십시오.
일부 외부 시스템과 실제로 동기화하는 경우 Effect가 상태를 업데이트해야 하는 이유와 조건에 대해 생각해 보세요. 컴포넌트의 시각적 출력에 영향을 미치는 변경 사항이 있습니까? 렌더링에 사용되지 않는 일부 데이터를 추적해야 하는 경우 참조(다시 렌더링을 트리거하지 않음)가 더 적합할 수 있습니다. Effect가 필요 이상으로 상태를 업데이트하고 다시 렌더링을 트리거하지 않는지 확인하세요.
마지막으로, Effect가 적시에 상태를 업데이트하지만 여전히 루프가 있는 경우 해당 상태 업데이트로 인해 Effect의 종속성 중 하나가 변경되기 때문입니다. 종속성 변경 사항을 디버그하는 방법을 읽어보세요.
cleanup 함수는 마운트 해제 중뿐만 아니라 종속성이 변경된 모든 다시 렌더링 전에 실행됩니다. 또한 개발 중에 React는 컴포넌트가 마운트된 직후에 setup + cleanup을 한 번 더 실행합니다.
해당 setup 코드가 없는 cleanup 코드가 있는 경우 일반적으로 code smell 입니다.
useEffect(() => {
// 🔴 Avoid: Cleanup logic without corresponding setup logic
return () => {
doSomething();
};
}, []);
정리 로직은 설정 로직과 "대칭"이어야 하며, 설정이 수행한 작업을 중지하거나 실행 취소해야 합니다:
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]);
Effect가 브라우저가 화면을 그리는 것을 차단해야 하는 경우 useEffect를 useLayoutEffect로 바꾸세요. 대부분의 Effect에는 이것이 필요하지 않습니다. 이는 브라우저를 그리기 전에 Effect를 실행하는 것이 중요한 경우에만 필요합니다. 예를 들어 사용자가 보기 전에 도구 설명을 측정하고 위치를 지정하는 경우입니다.
오... 솔직히 useEffect는 꽤나 많이 사용해서 기대하지 않았는데 알게 모르게 도움이 많이 되는 글이네요.