반응형값 의존성 지정
Effect의 의존성 "선택"할 수 없다는 점에 유의하라.
Effect 코드에서 사용하는 모든 반응형 값은 의존성으로 선언되어야 한다. Effect의 의존성 배열은 코드에 의해 결정된다.
function ChatRoom({ roomId }) { // 이것은 반응형 값입니다
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // 이것도 반응형 값입니다
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // 이 Effect는 이 반응형 값들을 읽습니다
connection.connect();
return () => connection.disconnect();
}, [serverUrl, roomId]); // ✅ 그래서 이 값들을 Effect의 의존성으로 지정해야 합니다
// ...
}
severUrl 또는 roomId가 변경될 때 마다 Effect는 새로운 값을 이용해 채팅을 다시 연결할 것이다.
반응형 값에는 props와 컴포넌트 내부에 선언된 모든 변수나 함수들이 포함된다. roomId와 serverUrl은 반응형 값이므로 이들을 의존성에서 제거하면 안된다. 이들을 누락했을 대 린터가 리액트 환경에 맞게 설정되어 있었다면 린터는 이것을 수정해야 하는 실수로 표시한다.
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'; // 더 이상 반응형 값이 아님
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ 모든 의존성이 선언됨
// ...
}
이제 serverUrl은 반응형 값이 아니며 (리렌더링 될 때 변경되지 않을 것이므로), 의존성에 추가할 필요가 없다. Effect의 코드가 어떤 반응형 값도 사용하지 않는다면 그 의존성 목록은 비어있어야 합니다. ([ ])
const serverUrl = 'https://localhost:1234'; // 더 이상 반응형 값이 아님
const roomId = 'music'; // 더 이상 반응형 값이 아님
function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ 모든 의존성이 선언됨
// ...
}
의존성이 비어있는 Effect는 컴포넌트의 props나 state가 변경되도 다시 실행되지 않는다.
주의!
기존의 코드 베이스에서 아래와 같이 린터를 억제하고 있는 일부 Effect가 있을 수 있다.
useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);
의존성이 코드와 일치하지 않을 때 버그가 도입될 위험이 크다. 린터를 억제함으로써 Effect가 의존하는 값에 대해 React가 거짓말을 하게 된다. 린터를 속이는 대신 이러한 값들이 불필요하다는 것을 증명하라.
Effect에서 이전 state를 기반으로 state 업데이트하기
Effect에서 이전 state를 기반으로 state를 업데이트 하려면 문제가 발생할 수 있다.
function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(count + 1); // 초마다 카운터를 증가시키고 싶습니다...
}, 1000)
return () => clearInterval(intervalId);
}, [count]); // 🚩 ... 하지만 'count'를 의존성으로 명시하면 항상 인터벌이 초기화됩니다.
// ...
}
count가 반응형 값이므로 반드시 의존성 배열에 추가해야 한다. 그러나 count가 변경되는 것은 Effect가 정리 된 후 다시 설정되는 것을 야기하므로 count는 계속 증가할 것이다.
이러한 현상을 방지하려면 c => c+1 state 변경함수를 setCount에 추가해라.
import { useState, useEffect } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setCount(c => c + 1); // ✅ State 업데이터를 전달
}, 1000);
return () => clearInterval(intervalId);
}, []); // ✅ 이제 count는 의존성이 아닙니다
return <h1>{count}</h1>;
}
c +> c+1 을 count +1 대신 전달하고 있으므로, Effect는 더이상 count에 의존하지 않는다. 이 수정으로 인해 count가 변경될 대 마다 Effect가 정리 및 설정을 다시 실행할 필요가 없게 된다.
불필요한 객체 의존성 제거하기
Effect가 렌더링 중에 생성된 객체나 함수에 의존하는 경우, 너무 자주 실행될 수 있다. 예를들어 이 Effect는 매 렌더링 후에 다시 연결된다. 이는 렌더링마다 options 객체가 다르기 때문이다.
const serverUrl = 'https://localhost:1234';
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = { // 🚩 이 객체는 재 렌더링 될 때마다 새로 생성됩니다
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options); // 객체가 Effect 안에서 사용됩니다
connection.connect();
return () => connection.disconnect();
}, [options]); // 🚩 결과적으로, 의존성이 재 렌더링 때마다 다릅니다
// ...
리렌더링 마다 함수를 처음부터 생성하는 것 그 자체로는문제가되지않고, 이를 최적화할 필요는 없다. 그러나 이 것을 Effect의 의존성으로 사용하는 경우 Effect가 리렌더링 후마다 다시 실행되게 한다.
렌더링 중에 생성된 객체를 의존성으로 사용하는 것을 피해라. 대신 객체를 Effect 내에서 생성해라.
Effect에서 최신 props와 state를 읽기
기본적으로 Effect에서 반응형 값을 읽을 때는 해당 값을 의존성에 추가해야 한다. 이렇게 하면 Effect가 해당 값의 모든 변경에 반응 하게 된다. 대부분의 의존성에서 원하는 동작이다.
그러나 때로는 Effect에서 최신 props와 state를 반응하지 않고 읽고 싶을 수 있다. 예를 들어 페이지 방문마다 쇼핑 카트에 담긴 항목 수를 기록하고 싶다고 가정하자.
function Page({ url, shoppingCart }) {
useEffect(() => {
logVisit(url, shoppingCart.length);
}, [url, shoppingCart]); // ✅ 모든 의존성이 선언됨
// ...
}
url 변경마다 새로운 페이지 방문을 기록하고 싶지만 shoppingCart만 변경되는 경우에는 기록하고 싶지 않다면 어떻게 해야할까?
shoppinfCart를 의존성에서 제외하면 반응성 규칙을 어기게 된다. Effect 내에서 호출되는 코드이지만 변경에 반응하지 않기를 원한다면 useEffectEvent Hook을 사용하여 Effect Event를 선언하고 shoppingCart를 읽는 코드를 그 안에 이동시킬 수 있다.
function Page({ url, shoppingCart }) {
const onVisit = useEffectEvent(visitedUrl => {
logVisit(visitedUrl, shoppingCart.length)
});
useEffect(() => {
onVisit(url);
}, [url]); // ✅ 모든 의존성이 선언됨
// ...
}
Effect Event 는 반응적이지 않으며 Effect의 의존성에서 배제되어야 한다. Effect Event에는 비 반응형 코드(Effect Event 로직은 최신 props와 state를 읽을 수 있음)를 배치할 수 있다. onVisit 내의 shoppingCart를 읽음으로써 shoppingCart의 변경으로 인한 Effect의 재실행을 방지한다.
서버 클라이언트에서 다른 컨텐츠를 표시하기
앱이 서버 렌더링을 사용하는 경우 (직접 또는 프레임워크를 통해) 컴포넌트는 두 가지 다른 환경에서 렌더링된다. 서버에서는 초기 HTML을 생성하기 위해 렌더링 되고, 클라이언트에서는 React가 이벤트 핸들러를 해당 HTML에 연결하기 위해 다시 렌더링 코드를 실행한다. 이것이 hydration이 작동하려면 초기 렌더링 출력이 서버와 클라이언트에서 동일해야 하는 이유이다.
드물게 클라이언트에서 다른 내용을 표시해야할 수 있다. 예를 들어 앱이 localStorage에서 일부 데이터를 읽는 경우, 이를 서버에서 구현할 수 없다.
function MyComponent() {
const [didMount, setDidMount] = useState(false);
useEffect(() => {
setDidMount(true);
}, []);
if (didMount) {
// ... 클라이언트 전용 JSX 반환 ...
} else {
// ... 초기 JSX 반환 ...
}
}
앱이 로딩중인 동안 사용자는 초기 렌더링 출력을 볼 것이다. 그 다음 로딩 및 hydration이 완료되면 Effect가 실행되어 didMount를 true로 설정하면서 다시 렌더링이 동작한다. 이로써 클라이언트 전용 렌더링 출력으로 전환된다. Effect는 서버에서 실행되지 않으므로 초기 서버 렌더링 중의 didMount는 false가 된다.
이 패턴은 적절히 사용해야 한다. 느린 연결 환경을 가진 사용자는 초기 렌더링 화면을 상당한 시간 도안 볼 것임로 컴포넌트의 모양을 급변시키지 않는 것이 좋다. 많은 경우에는 CSS를 사용하여 조건부로 다양한 것들을 표시하는 방법으로 대처할 수 있다.