import { forwardRef, useImperativeHandle } from 'react';
const MyInput = forwardRef(function MyInput(props, ref) {
useImperativeHandle(ref, () => {
return {
// ... your methods ...
// ... 메서드는 여기에 작성합니다 ...
};
}, []);
// ...
// App.js
import { useRef } from 'react';
import Post from './Post.js';
export default function Page() {
const postRef = useRef(null);
function handleClick() {
postRef.current.scrollAndFocusAddComment();
}
return (
<>
<button onClick={handleClick}>
Write a comment
</button>
<Post ref={postRef} />
</>
);
}
// Post.js
import { forwardRef, useRef, useImperativeHandle } from 'react';
import CommentList from './CommentList.js';
import AddComment from './AddComment.js';
const Post = forwardRef((props, ref) => {
const commentsRef = useRef(null);
const addCommentRef = useRef(null);
useImperativeHandle(ref, () => {
return {
scrollAndFocusAddComment() {
commentsRef.current.scrollToBottom();
addCommentRef.current.focus();
}
};
}, []);
return (
<>
<article>
<p>Welcome to my blog!</p>
</article>
<CommentList ref={commentsRef} />
<AddComment ref={addCommentRef} />
</>
);
});
export default Post;
Object.is?
Object.is() 로 비교하는 것과 === 에는 어떤 차이가 있을까?
0 === -0 // true
-0 === -0 // true
NaN === 0/0 // false
Object.is(0, -0); // false
Object.is(-0, -0); // true
Object.is(NaN, 0/0); // true
Object.is()는 -0, 0이면 다르다고 인식, NaN === 0/0 와 같이 결과값이 NaN일 경우 같다라고 인식한다.
그리고 이 부분에서 헷갈린 것이 있는데, 객체가 객체를 갖고 있는 이중객체일 경우 비교가 제대로 되지 않아 문제가 생기지 않을까? 헷갈렸는데 다시 생각해보니 비교할 객체와 변경된 객체들의 각각의 저장 위치가 달라서 애초에 다르다고 인식한다. 상태가 변경되는 타이밍에 어짜피 해당 객체는 달라졌다고 인식되는 것이다.
Strict 모드가 켜져 있으면 React는 첫 번째 실제 셋업 전에 개발 전용의 셋업+클린업 사이클을 한 번 더 실행함. 문제가 발생하면 클린업 기능을 구현해야 함.
의존성 중 일부가 컴포넌트 내부에 정의된 객체 또는 함수일 경우 Effect가 필요 이상으로 자주 다시 실행될 위험이 존재. 이 문제를 해결하려면 불필요한 객체 및 함수 의존성을 제거해야 함 혹은 Effect 외부에서 state 업데이트 추출 및 비반응형 로직을 제거할 수도 있음
Effect가 시각적인 작업(예: 툴팁 위치 지정)을 하고 있고, 지연이 눈에 띄는 경우 useEffect를 useLayoutEffect로 대체해야 함.
Effect는 클라이언트에서만 실행되며, 서버 렌더링 중에는 실행되지 않음
컴포넌트가 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 연결 상태를 유지해야 할 수도 있음. 이것을 외부라고 함.
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]);
// ...
}
위의 ChatRoom 컴포넌트가 페이지에 추가되면 초기 serverUrl 및 roomId로 채팅방에 연결됩니다. 다시 렌더링한 결과 serverUrl 또는 roomId가 변경되면(예: 사용자가 드롭다운에서 다른 채팅방을 선택하는 경우) Effect는 이전 채팅방과의 연결을 끊고 다음 채팅방에 연결합니다. ChatRoom 컴포넌트가 페이지에서 제거되면 Effect는 마지막으로 연결을 끊습니다.
Effect는 탈출구. Effect를 수동으로 작성해야 하는 경우가 자주 발생한다면 이는 컴포넌트가 의존하는 일반적인 동작에 대한 커스텀 훅을 추출해야 한다는 신호일 수 있습니다.
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
});
// ...
외부 시스템을 컴포넌트의 특정 prop이나 state와 동기화하고 싶을 때, React 없이 작성된 타사 맵 위젯이나 비디오 플레이어 컴포넌트가 있는 경우에 사용
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}
/>
);
}
조건 경합
// App.js
import React, { useState } from "react";
import DataDisplayer from "./DataDisplayer";
export default function App() {
const [currentId, setCurrentId] = useState(1);
const handleClick = () => {
const idToFetch = Math.round(Math.random() * 80);
setCurrentId(idToFetch);
};
return (
<React.Fragment>
<div>
<p>Requesting ID: {currentId}</p>
<button type="button" onClick={handleClick}>
Fetch data!
</button>
</div>
<hr />
<DataDisplayer id={currentId} />
</React.Fragment>
);
}
// DataDisplayer.js
import React, { useEffect, useState } from "react";
export default function DataDisplayer(props) {
const [data, setData] = useState(null);
const [fetchedId, setFetchedId] = useState(null);
useEffect(() => {
const fetchData = async () => {
setTimeout(async () => {
const response = await fetch(
`https://swapi.dev/api/people/${props.id}/`
);
const newData = await response.json();
setFetchedId(props.id);
setData(newData);
}, Math.round(Math.random() * 12000));
};
fetchData();
}, [props.id]);
if (data) {
return (
<div>
<p style={{ color: fetchedId === props.id ? "green" : "red" }}>
Displaying Data for: {fetchedId}
</p>
<p>{data.name}</p>
</div>
);
} else {
return null;
}
}
위와 같은 데이터 경합이 발생했을 땐 언마운트 되는 시점에는 set함수가 실행되지 않도록 처리해 줘야함.
직접 데이터를 페칭하는 작업을 반복적으로 작성하면 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기 어려워짐.
이펙트는 서버에서 실행되지 않음. 즉, 서버에서 렌더링되는 초기 HTML에는 데이터가 없는 로딩 state만 포함됨. 즉, 모든 js를 다운로드하고 앱을 렌더링해야만 데이터를 로드할 수 있음.
effect에서 직접 페칭하면 네트워크 워터폴이 만들어지기 쉬워지는 것임. 또, effect에서 직접 페칭한다는 것은 일반적으로 데이터를 데이터에 미리 로드하거나 캐시하지 않는다는 의미임.
조건 경합과 같은 버그가 발생하지 않는 방식으로 fetch 호출을 작성하려면 상용구 코드가 상당히 많이 필요함
framework를 사용하는 경우 프레임워크 빌트인 데이터 페칭 메커니즘을 사용할 것을 추천. 그게 아니라면 client-side 캐시를 사용하거나 구축할 것을 고려. (React Query, useSWR, React Router 6.4+ 등)
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'
// ...
}
그럴 경우, 의존성을 제거하려면 의존성이어ㅑ 할 필요가 없음을 린터에게 증명해야 함.
예를 들어, 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();
}, []); // ✅ All dependencies declared
// ...
}
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를 다시 클린업하고 셋업해줘야 함. 아래와 같이 작성하는 것이 이상적임
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>;
}
// 아직 출시되지 않은 실험적 API 에 대한 설명
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ All dependencies declared
// ...
}
url이 변경될 때마다 페이지 방문을 기록하되 shoppingCart만 변경되지 않는 경우는 기록하지 않으려면?
useEffectEvent 를 사용하기. 하지만 지금은 작업중이라 사용할 수 없는듯.
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ All dependencies declared
// ...
}
서버 렌더링을 사용하는 앱의 경우, 컴포넌트는 두 가지 다른 환경에서 렌더링됨. 서버는 초기 HTML을 생성하기 위해 렌더링됨. 클라이언트에서 React는 렌더링 코드를 다시 실행하여 이벤트 핸들러를 해당 HTML에 첨부할 수 있도록 함. hydration이 작동하려면 클라이언트와 서버의 첫 렌더링 결과가 동일해야 함
드물지만 클라이언트에 다른 콘텐츠를 표시해야 하는 경우가 있을 수 있음. 예를 들어 localStorage에서 일부 데이터를 읽는 경우 서버에서 이를 수행할 수 없음. 일반적으로 이를 구현하는 방법은 다음과 같음
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... return client-only JSX ...
} else {
// ... return initial JSX ...
}
}
앱이 로드되는 동안 사용자는 초기 렌더링 결과물을 봄. 그런 다음 앱이 로드 및 hydrated 되면 Effect 가 실행. Effect는 서버에서 실행되지 않기 때문에 서버 렌더링 중에는 didMount 는 false임
단, 이 패턴은 되도록 사용을 아껴야 함.