Removing Effect DependenciesEffect 의존성 제거하기

When you write an Effect, the linter will verify that you’ve included every reactive value (like props and state) that the Effect reads in the list of your Effect’s dependencies. This ensures that your Effect remains synchronized with the latest props and state of your component. Unnecessary dependencies may cause your Effect to run too often, or even create an infinite loop. Follow this guide to review and remove unnecessary dependencies from your Effects. Effect를 작성하면 린터는 Effect의 의존성 목록에 Effect가 읽는 모든 반응형 값(예: props 및 state)을 포함했는지 확인합니다. 이렇게 하면 Effect가 컴포넌트의 최신 props 및 state와 동기화 상태를 유지할 수 있습니다. 불필요한 의존성으로 인해 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수도 있습니다. 이 가이드를 따라 Effect에서 불필요한 의존성을 검토하고 제거하세요.

You will learn학습 내용

  • How to fix infinite Effect dependency loops
  • What to do when you want to remove a dependency
  • How to read a value from your Effect without “reacting” to it
  • How and why to avoid object and function dependencies
  • Why suppressing the dependency linter is dangerous, and what to do instead
  • Effect 의존성 무한 루프를 수정하는 방법
  • 의존성을 제거하고자 할 때 해야 할 일
  • Effect에 “반응”하지 않고 Effect에서 값을 읽는 방법
  • 객체와 함수 의존성을 피하는 방법과 이유
  • 의존성 린터를 억제하는 것이 위험한 이유와 대신 할 수 있는 일

Dependencies should match the code의존성은 코드와 일치해야 합니다

When you write an Effect, you first specify how to start and stop whatever you want your Effect to be doing: Effect를 작성할 때는 먼저 Effect가 수행하기를 원하는 작업을 시작하고 중지하는 방법을 지정합니다:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
// ...
}

Then, if you leave the Effect dependencies empty ([]), the linter will suggest the correct dependencies: 그런 다음 Effect 의존성을 비워두면([]) 린터가 올바른 의존성을 제안합니다:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Fix the mistake here!
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

Fill them in according to what the linter says: 린터에 표시된 내용에 따라 채우세요:

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}

Effects “react” to reactive values. Since roomId is a reactive value (it can change due to a re-render), the linter verifies that you’ve specified it as a dependency. If roomId receives a different value, React will re-synchronize your Effect. This ensures that the chat stays connected to the selected room and “reacts” to the dropdown: Effect는 반응형 값에 “반응”합니다. roomId는 반응형 값이므로(리렌더링으로 인해 변경될 수 있음), 린터는 이를 의존성으로 지정했는지 확인합니다. roomId가 다른 값을 받으면 React는 Effect를 다시 동기화합니다. 이렇게 하면 채팅이 선택된 방에 연결된 상태를 유지하고 드롭다운에 ‘반응’합니다:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);
  return <h1>Welcome to the {roomId} room!</h1>;
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

To remove a dependency, prove that it’s not a dependency의존성을 제거하려면 의존성이 아님을 증명하세요

Notice that you can’t “choose” the dependencies of your Effect. Every reactive value used by your Effect’s code must be declared in your dependency list. The dependency list is determined by the surrounding code: Effect의 의존성을 “선택”할 수 없다는 점에 유의하세요. Effect의 코드에서 사용되는 모든 반응형 값은 의존성 목록에 선언되어야 합니다. 의존성 목록은 주변 코드에 의해 결정됩니다:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) { // This is a reactive value
// roomId는 반응형 값임
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads that reactive value
// 이 Effect는 반응형값을 읽음
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ So you must specify that reactive value as a dependency of your Effect
// ✅ 그러므로 해당 반응형 값을 Effect의 의존성 목록에 추가해야함
// ...
}

Reactive values include props and all variables and functions declared directly inside of your component. Since roomId is a reactive value, you can’t remove it from the dependency list. The linter wouldn’t allow it: 반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수 및 함수가 포함됩니다. roomId는 반응형 값이므로 의존성 목록에서 제거할 수 없습니다. 린터가 허용하지 않습니다:

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // 🔴 React Hook useEffect has a missing dependency: 'roomId'
// ...
}

And the linter would be right! Since roomId may change over time, this would introduce a bug in your code. 그리고 린터가 맞을 것입니다! roomId는 시간이 지남에 따라 변경될 수 있으므로 코드에 버그가 발생할 수 있습니다.

To remove a dependency, “prove” to the linter that it doesn’t need to be a dependency. For example, you can move roomId out of your component to prove that it’s not reactive and won’t change on re-renders: 의존성을 제거하려면 해당 컴포넌트가 의존성이 될 필요가 없다는 것을 린터에 “증명”하세요. 예를 들어, roomId를 컴포넌트 밖으로 이동시켜도 반응하지 않고 리렌더링 시에도 변경되지 않음을 증명할 수 있습니다:

const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore
// 더이상 반응형 값이 아님

function ChatRoom() {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
}

Now that roomId is not a reactive value (and can’t change on a re-render), it doesn’t need to be a dependency: 이제 roomId는 반응형 값이 아니므로(리렌더링할 때 변경할 수 없으므로) 의존성이 될 필요가 없습니다:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';
const roomId = 'music';

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>Welcome to the {roomId} room!</h1>;
}

This is why you could now specify an empty ([]) dependency list. Your Effect really doesn’t depend on any reactive value anymore, so it really doesn’t need to re-run when any of the component’s props or state change. 이것이 빈([]) 의존성 목록을 지정할 수 있는 이유입니다. Effect는 더 이상 반응형 값에 의존하지 않으므로 컴포넌트의 props나 state가 변경될 때 Effect를 다시 실행할 필요가 없습니다.

To change the dependencies, change the code의존성을 변경하려면 코드를 변경하세요

You might have noticed a pattern in your workflow: 작업흐름에서 패턴을 발견했을 수도 있습니다:

  1. First, you change the code of your Effect or how your reactive values are declared.
  2. Then, you follow the linter and adjust the dependencies to match the code you have changed.
  3. If you’re not happy with the list of dependencies, you go back to the first step (and change the code again).
  1. 먼저 Effect의 코드 또는 반응형 값 선언 방식을 변경합니다.
  2. 그런 다음, 변경한 코드에 맞게 의존성을 조정합니다.
  3. 의존성 목록이 마음에 들지 않으면 첫 번째 단계로 돌아가서 코드를 다시 변경합니다. (그리고 코드를 다시 변경하세요).

The last part is important. If you want to change the dependencies, change the surrounding code first. You can think of the dependency list as a list of all the reactive values used by your Effect’s code. You don’t choose what to put on that list. The list describes your code. To change the dependency list, change the code. 마지막 부분이 중요합니다. 의존성을 변경하려면 먼저 주변 코드를 변경하세요. 의존성 목록은 Effect의 코드에서 사용하는 모든 반응형 값의 목록이라고 생각하면 됩니다. 이 목록에 무엇을 넣을지는 사용자가 선택하지 않습니다. 이 목록은 코드를 설명합니다. 의존성 목록을 변경하려면 코드를 변경하세요.

This might feel like solving an equation. You might start with a goal (for example, to remove a dependency), and you need to “find” the code matching that goal. Not everyone finds solving equations fun, and the same thing could be said about writing Effects! Luckily, there is a list of common recipes that you can try below. 이것은 방정식을 푸는 것처럼 느껴질 수 있습니다. 예를 들어, 의존성 제거와 같은 목표를 설정하고 그 목표에 맞는 코드를 “찾아야” 합니다. 모든 사람이 방정식을 푸는 것을 재미있어하는 것은 아니며, Effect를 작성할 때도 마찬가지입니다! 다행히도 아래에 시도해 볼 수 있는 일반적인 레시피 목록이 있습니다.

Pitfall | 함정

If you have an existing codebase, you might have some Effects that suppress the linter like this: 기존 코드베이스가 있는 경우 이와 같이 린터를 억제하는 Effect가 있을 수 있습니다:

useEffect(() => {
// ...
// 🔴 Avoid suppressing the linter like this:
// eslint-ignore-next-line react-hooks/exhaustive-deps
}, []);

When dependencies don’t match the code, there is a very high risk of introducing bugs. By suppressing the linter, you “lie” to React about the values your Effect depends on. 의존성이 코드와 일치하지 않으면 버그가 발생할 위험이 매우 높습니다. 린터를 억제하면 Effect가 의존하는 값에 대해 React에 “거짓말”을 하게 됩니다.

Instead, use the techniques below. 대신 다음에 소개할 기술을 사용하세요.

Deep Dive | 심층 탐구

Why is suppressing the dependency linter so dangerous?의존성 린터를 억제하는 것이 왜 위험한가요?

Suppressing the linter leads to very unintuitive bugs that are hard to find and fix. Here’s one example: 린터를 억제하면 매우 직관적이지 않은 버그가 발생하여 찾아서 수정하기가 어렵습니다. 한 가지 예를 들어보겠습니다:

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);
  const [increment, setIncrement] = useState(1);

  function onTick() {
	setCount(count + increment);
  }

  useEffect(() => {
    const id = setInterval(onTick, 1000);
    return () => clearInterval(id);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <>
      <h1>
        Counter: {count}
        <button onClick={() => setCount(0)}>Reset</button>
      </h1>
      <hr />
      <p>
        Every second, increment by:
        <button disabled={increment === 0} onClick={() => {
          setIncrement(i => i - 1);
        }}></button>
        <b>{increment}</b>
        <button onClick={() => {
          setIncrement(i => i + 1);
        }}>+</button>
      </p>
    </>
  );
}

Let’s say that you wanted to run the Effect “only on mount”. You’ve read that empty ([]) dependencies do that, so you’ve decided to ignore the linter, and forcefully specified [] as the dependencies. “마운트할 때만” Effect를 실행하고 싶다고 가정해 봅시다. 빈([]) 의존성이 그렇게 한다는 것을 읽었으므로 린터를 무시하고 [] 의존성을 강제로 지정하기로 결정했습니다.

This counter was supposed to increment every second by the amount configurable with the two buttons. However, since you “lied” to React that this Effect doesn’t depend on anything, React forever keeps using the onTick function from the initial render. During that render, count was 0 and increment was 1. This is why onTick from that render always calls setCount(0 + 1) every second, and you always see 1. Bugs like this are harder to fix when they’re spread across multiple components. 이 카운터는 두 개의 버튼으로 설정할 수 있는 양만큼 매초마다 증가해야 합니다. 하지만 이 Effect가 아무 것도 의존하지 않는다고 React에 “거짓말”을 했기 때문에, React는 초기 렌더링에서 계속 onTick 함수를 사용합니다. 이 렌더링에서 count0이었고 increment1이었습니다. 그래서 이 렌더링의 onTick은 항상 매초마다 setCount(0 + 1)를 호출하고 항상 1이 표시됩니다. 이와 같은 버그는 여러 컴포넌트에 분산되어 있을 때 수정하기가 더 어렵습니다.

There’s always a better solution than ignoring the linter! To fix this code, you need to add onTick to the dependency list. (To ensure the interval is only setup once, make onTick an Effect Event.) 린터를 무시하는 것보다 더 좋은 해결책은 항상 있습니다! 이 코드를 수정하려면 의존성 목록에 onTick을 추가해야 합니다. (interval을 한 번만 설정하려면 onTick을 Effect Event로 만드세요.)

We recommend treating the dependency lint error as a compilation error. If you don’t suppress it, you will never see bugs like this. The rest of this page documents the alternatives for this and other cases. 의존성 린트 오류는 컴파일 오류로 처리하는 것이 좋습니다. 이를 억제하지 않으면 이와 같은 버그가 발생하지 않습니다. 이 페이지의 나머지 부분에서는 이 경우와 다른 경우에 대한 대안을 설명합니다.

Removing unnecessary dependencies불필요한 의존성 제거하기

Every time you adjust the Effect’s dependencies to reflect the code, look at the dependency list. Does it make sense for the Effect to re-run when any of these dependencies change? Sometimes, the answer is “no”: 코드를 반영하기 위해 Effect의 의존성을 조정할 때마다 의존성 목록을 살펴보십시오. 이러한 의존성 중 하나라도 변경되면 Effect가 다시 실행되는 것이 합리적일까요? 가끔 대답은 “아니오”입니다:

  • You might want to re-execute different parts of your Effect under different conditions.
  • You might want to only read the latest value of some dependency instead of “reacting” to its changes.
  • A dependency may change too often unintentionally because it’s an object or a function.
  • 다른 조건에서 Effect의 다른 부분을 다시 실행하고 싶을 수도 있습니다.
  • 일부 의존성의 변경에 “반응”하지 않고 “최신 값”만 읽고 싶을 수도 있습니다.
  • 의존성은 객체나 함수이기 때문에 의도치 않게 너무 자주 변경될 수 있습니다.

To find the right solution, you’ll need to answer a few questions about your Effect. Let’s walk through them. 올바른 해결책을 찾으려면 Effect에 대한 몇 가지 질문에 답해야 합니다. 몇 가지 질문을 살펴봅시다.

Should this code move to an event handler?이 코드를 이벤트 핸들러로 옮겨야 하나요?

The first thing you should think about is whether this code should be an Effect at all. 가장 먼저 고려해야 할 것은 이 코드가 Effect가 되어야 하는지 여부입니다.

Imagine a form. On submit, you set the submitted state variable to true. You need to send a POST request and show a notification. You’ve put this logic inside an Effect that “reacts” to submitted being true: form을 상상해 봅시다. 제출할 때 submitted state 변수를 true로 설정합니다. POST 요청을 보내고 알림을 표시해야 합니다. 이 로직은 submittedtrue가 될 때 “반응”하는 Effect 안에 넣었습니다:

function Form() {
const [submitted, setSubmitted] = useState(false);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!');
}
}, [submitted]);

function handleSubmit() {
setSubmitted(true);
}

// ...
}

Later, you want to style the notification message according to the current theme, so you read the current theme. Since theme is declared in the component body, it is a reactive value, so you add it as a dependency: 나중에 현재 테마에 따라 알림 메시지의 스타일을 지정하고 싶으므로 현재 테마를 읽습니다. theme는 컴포넌트 본문에서 선언 되었기때문에 이는 반응형 값이므로 의존성으로 추가합니다:

function Form() {
const [submitted, setSubmitted] = useState(false);
const theme = useContext(ThemeContext);

useEffect(() => {
if (submitted) {
// 🔴 Avoid: Event-specific logic inside an Effect
post('/api/register');
showNotification('Successfully registered!', theme);
}
}, [submitted, theme]); // ✅ All dependencies declared

function handleSubmit() {
setSubmitted(true);
}

// ...
}

By doing this, you’ve introduced a bug. Imagine you submit the form first and then switch between Dark and Light themes. The theme will change, the Effect will re-run, and so it will display the same notification again! 이렇게 하면 버그가 발생하게 됩니다. 먼저 form을 제출한 다음 어두운 테마와 밝은 테마 간 전환한다고 가정해 보겠습니다. theme가 변경되고 Effect가 다시 실행되어 동일한 알림이 다시 표시됩니다!

The problem here is that this shouldn’t be an Effect in the first place. You want to send this POST request and show the notification in response to submitting the form, which is a particular interaction. To run some code in response to particular interaction, put that logic directly into the corresponding event handler: 여기서 문제는 이것이 애초에 Effect가 아니어야 한다는 점입니다. 이 POST 요청을 보내고 특정 상호작용인 form 제출에 대한 응답으로 알림을 표시하고 싶다는 것입니다. 특정 상호작용에 대한 응답으로 일부 코드를 실행하려면 해당 로직을 해당 이벤트 핸들러에 직접 넣어야 합니다:

function Form() {
const theme = useContext(ThemeContext);

function handleSubmit() {
// ✅ Good: Event-specific logic is called from event handlers
post('/api/register');
showNotification('Successfully registered!', theme);
}

// ...
}

Now that the code is in an event handler, it’s not reactive—so it will only run when the user submits the form. Read more about choosing between event handlers and Effects and how to delete unnecessary Effects. 이제 코드가 이벤트 핸들러에 있고, 이는 반응형 코드가 아니므로 사용자가 form을 제출할 때만 실행됩니다. 이벤트 핸들러와 Effect 중에서 선택하는 방법불필요한 Effect를 삭제하는 방법에 대해 자세히 알아보세요.

Is your Effect doing several unrelated things?Effect가 여러 관련 없는 일을 하고 있나요?

The next question you should ask yourself is whether your Effect is doing several unrelated things. 다음으로 스스로에게 물어봐야 할 질문은 Effect가 서로 관련이 없는 여러 가지 작업을 수행하고 있는지 여부입니다.

Imagine you’re creating a shipping form where the user needs to choose their city and area. You fetch the list of cities from the server according to the selected country to show them in a dropdown: 사용자가 도시와 지역을 선택해야 하는 배송 form을 만든다고 가정해 보겠습니다. 선택한 country에 따라 서버에서 cities 목록을 페치해서 드롭다운에 표시합니다:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

// ...

This is a good example of fetching data in an Effect. You are synchronizing the cities state with the network according to the country prop. You can’t do this in an event handler because you need to fetch as soon as ShippingForm is displayed and whenever the country changes (no matter which interaction causes it). Effect에서 데이터를 페칭하는 좋은 예시입니다. country props에 따라 cities state를 네트워크와 동기화하고 있습니다. ShippingForm이 표시되는 즉시 그리고 country가 변경될 때마다 (어떤 상호작용이 원인이든 상관없이) 데이터를 가져와야 하므로 이벤트 핸들러에서는 이 작업을 수행할 수 없습니다.

Now let’s say you’re adding a second select box for city areas, which should fetch the areas for the currently selected city. You might start by adding a second fetch call for the list of areas inside the same Effect: 이제 도시 지역에 대한 두 번째 셀렉트박스를 추가하여 현재 선택된 cityareas을 페치한다고 가정해 보겠습니다. 동일한 Effect 내에 지역 목록에 대한 두 번째 fetch 호출을 추가하는 것으로 시작할 수 있습니다:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);

useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared

// ...

However, since the Effect now uses the city state variable, you’ve had to add city to the list of dependencies. That, in turn, introduced a problem: when the user selects a different city, the Effect will re-run and call fetchCities(country). As a result, you will be unnecessarily refetching the list of cities many times. 하지만 이제 Effect가 city state 변수를 사용하므로 의존성 목록에 city를 추가해야 했습니다. 이로 인해 사용자가 다른 도시를 선택하면 Effect가 다시 실행되어 fetchCities(country)를 호출하는 문제가 발생했습니다. 결과적으로 불필요하게 도시 목록을 여러 번 다시 페치하게 됩니다.

The problem with this code is that you’re synchronizing two different unrelated things: 이 코드의 문제점은 서로 관련이 없는 두 가지를 동기화하고 있다는 것입니다.

  1. You want to synchronize the cities state to the network based on the country prop.
  2. You want to synchronize the areas state to the network based on the city state.
  1. country props를 기반으로 cities state를 네트워크에 동기화하려고 합니다.
  2. city state를 기반으로 areas state를 네트워크에 동기화하려고 합니다.

Split the logic into two Effects, each of which reacts to the prop that it needs to synchronize with: 로직을 두 개의 Effect로 분할하고, 각 Effect는 동기화해야 하는 props에 반응합니다:

function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
return () => {
ignore = true;
};
}, [country]); // ✅ All dependencies declared

const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
let ignore = false;
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
return () => {
ignore = true;
};
}
}, [city]); // ✅ All dependencies declared

// ...

Now the first Effect only re-runs if the country changes, while the second Effect re-runs when the city changes. You’ve separated them by purpose: two different things are synchronized by two separate Effects. Two separate Effects have two separate dependency lists, so they won’t trigger each other unintentionally. 이제 첫 번째 Effect는 country가 변경될 때만 다시 실행되고, 두 번째 Effect는 city가 변경될 때 다시 실행됩니다. 목적에 따라 분리했으니, 서로 다른 두 가지가 두 개의 개별 Effect에 의해 동기화됩니다. 두 개의 개별 Effect에는 두 개의 개별 의존성 목록이 있으므로 의도치 않게 서로를 촉발하지 않습니다.

The final code is longer than the original, but splitting these Effects is still correct. Each Effect should represent an independent synchronization process. In this example, deleting one Effect doesn’t break the other Effect’s logic. This means they synchronize different things, and it’s good to split them up. If you’re concerned about duplication, you can improve this code by extracting repetitive logic into a custom Hook. 최종 코드는 원본보다 길지만 Effect를 분할하는 것이 여전히 정확합니다. 각 Effect는 독립적인 동기화 프로세스를 나타내야 합니다. 이 예제에서는 한 Effect를 삭제해도 다른 Effect의 로직이 깨지지 않습니다. 즉, 서로 다른 것을 동기화하므로 분할하는 것이 좋습니다. 중복이 걱정된다면 반복되는 로직을 커스텀 Hook으로 추출하여 이 코드를 개선할 수 있습니다.

Are you reading some state to calculate the next state?다음 state를 계산하기 위해 어떤 state를 읽고 있나요?

This Effect updates the messages state variable with a newly created array every time a new message arrives: 이 Effect는 새 메시지가 도착할 때마다 새로 생성된 배열로 messages state 변수를 업데이트합니다:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
// ...

It uses the messages variable to create a new array starting with all the existing messages and adds the new message at the end. However, since messages is a reactive value read by an Effect, it must be a dependency: messages 변수를 사용하여 모든 기존 메시지로 시작하는 새 배열을 생성하고 마지막에 새 메시지를 추가합니다. 하지만 messages는 Effect에서 읽는 반응형 값이므로 의존성이어야 합니다:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...

And making messages a dependency introduces a problem. 그리고 messages를 의존성으로 만들면 문제가 발생합니다.

Every time you receive a message, setMessages() causes the component to re-render with a new messages array that includes the received message. However, since this Effect now depends on messages, this will also re-synchronize the Effect. So every new message will make the chat re-connect. The user would not like that! 메시지를 수신할 때마다 setMessages()는 컴포넌트가 수신된 메시지를 포함하는 새 messages 배열로 리렌더링하도록 합니다. 하지만 이 Effect는 이제 messages에 따라 달라지므로 Effect도 다시 동기화됩니다. 따라서 새 메시지가 올 때마다 채팅이 다시 연결됩니다. 사용자가 원하지 않을 것입니다!

To fix the issue, don’t read messages inside the Effect. Instead, pass an updater function to setMessages: 이 문제를 해결하려면 Effect 내에서 messages를 읽지 마세요. 대신 업데이터 함수setMessages에 전달하세요:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Notice how your Effect does not read the messages variable at all now. You only need to pass an updater function like msgs => [...msgs, receivedMessage]. React puts your updater function in a queue and will provide the msgs argument to it during the next render. This is why the Effect itself doesn’t need to depend on messages anymore. As a result of this fix, receiving a chat message will no longer make the chat re-connect. 이제 Effect가 messages 변수를 전혀 읽지 않는 것을 알 수 있습니다. msgs => [...msgs, receivedMessage]와 같은 업데이터 함수만 전달하면 됩니다. React는 업데이터 함수를 대기열에 넣고 다음 렌더링 중에 msgs 인수를 제공합니다. 이 때문에 Effect 자체는 더 이상 messages에 의존할 필요가 없습니다. 이 수정으로 인해 채팅 메시지를 수신해도 더 이상 채팅이 다시 연결되지 않습니다.

Do you want to read a value without “reacting” to its changes?값의 변경에 ‘반응’하지 않고 값을 읽고 싶으신가요?

Under Construction | 작업중

This section describes an experimental API that has not yet been released in a stable version of React. 이 섹션에서는 아직 안정된 버전의 React로 출시되지 않은 실험적인 API에 대해 설명합니다.

Suppose that you want to play a sound when the user receives a new message unless isMuted is true: 사용자가 새 메시지를 수신할 때 isMutedtrue가 아닌 경우 사운드를 재생하고 싶다고 가정해 보겠습니다:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
// ...

Since your Effect now uses isMuted in its code, you have to add it to the dependencies: 이제 Effect의 코드에서 isMuted를 사용하므로 의존성에 추가해야 합니다:

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});
return () => connection.disconnect();
}, [roomId, isMuted]); // ✅ All dependencies declared
// ...

The problem is that every time isMuted changes (for example, when the user presses the “Muted” toggle), the Effect will re-synchronize, and reconnect to the chat. This is not the desired user experience! (In this example, even disabling the linter would not work—if you do that, isMuted would get “stuck” with its old value.) 문제는 (사용자가 “Muted” 토글을 누르는 등) isMuted가 변경될 때마다 Effect가 다시 동기화되고 채팅에 다시 연결된다는 점입니다. 이는 바람직한 사용자 경험이 아닙니다! (이 예에서는 린터를 비활성화해도 작동하지 않습니다. 그렇게 하면 isMuted가 이전 값으로 ‘고착’됩니다.)

To solve this problem, you need to extract the logic that shouldn’t be reactive out of the Effect. You don’t want this Effect to “react” to the changes in isMuted. Move this non-reactive piece of logic into an Effect Event: 이 문제를 해결하려면 Effect에서 반응해서는 안 되는 로직을 추출해야 합니다. 이 Effect가 isMuted의 변경에 “반응”하지 않기를 원합니다. 이 비반응 로직을 Effect Event로 옮기면 됩니다:

import { useState, useEffect, useEffectEvent } from 'react';

function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
const [isMuted, setIsMuted] = useState(false);

const onMessage = useEffectEvent(receivedMessage => {
setMessages(msgs => [...msgs, receivedMessage]);
if (!isMuted) {
playSound();
}
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effect Events let you split an Effect into reactive parts (which should “react” to reactive values like roomId and their changes) and non-reactive parts (which only read their latest values, like onMessage reads isMuted). Now that you read isMuted inside an Effect Event, it doesn’t need to be a dependency of your Effect. As a result, the chat won’t re-connect when you toggle the “Muted” setting on and off, solving the original issue! Effect Event를 사용하면 Effect를 반응형 부분(roomId와 같은 반응형 값과 그 변경에 “반응”해야 하는)과 비반응형 부분(onMessageisMuted를 읽는 것처럼 최신 값만 읽는)으로 나눌 수 있습니다. 이제 Effect Event 내에서 isMuted를 읽었으므로 Effect의 의존성이 될 필요가 없습니다. 그 결과, “Muted” 설정을 켜고 끌 때 채팅이 다시 연결되지 않아 원래 문제가 해결되었습니다!

Wrapping an event handler from the propsprops를 이벤트 핸들러로 감싸기

You might run into a similar problem when your component receives an event handler as a prop: 컴포넌트가 이벤트 핸들러를 props로 받을 때 비슷한 문제가 발생할 수 있습니다:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onReceiveMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId, onReceiveMessage]); // ✅ All dependencies declared
// ...

Suppose that the parent component passes a different onReceiveMessage function on every render: 부모 컴포넌트가 렌더링할 때마다 다른 onReceiveMessage 함수를 전달한다고 가정해 보겠습니다:

<ChatRoom
roomId={roomId}
onReceiveMessage={receivedMessage => {
// ...
}}
/>

Since onReceiveMessage is a dependency, it would cause the Effect to re-synchronize after every parent re-render. This would make it re-connect to the chat. To solve this, wrap the call in an Effect Event: onReceiveMessage는 의존성이므로 부모가 리렌더링할 때마다 Effect가 다시 동기화됩니다. 그러면 채팅에 다시 연결됩니다. 이 문제를 해결하려면 호출을 Effect Event로 감싸세요:

function ChatRoom({ roomId, onReceiveMessage }) {
const [messages, setMessages] = useState([]);

const onMessage = useEffectEvent(receivedMessage => {
onReceiveMessage(receivedMessage);
});

useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
onMessage(receivedMessage);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...

Effect Events aren’t reactive, so you don’t need to specify them as dependencies. As a result, the chat will no longer re-connect even if the parent component passes a function that’s different on every re-render. Effect Event는 반응하지 않으므로 의존성으로 지정할 필요가 없습니다. 그 결과, 부모 컴포넌트가 리렌더링할 때마다 다른 함수를 전달하더라도 채팅이 더 이상 다시 연결되지 않습니다.

Separating reactive and non-reactive code반응형 코드와 비반응형 코드 분리

In this example, you want to log a visit every time roomId changes. You want to include the current notificationCount with every log, but you don’t want a change to notificationCount to trigger a log event. 이 예제에서는 roomId가 변경될 때마다 방문을 기록하려고 합니다. 모든 로그에 현재 notificationCount를 포함하고 싶지만 notificationCount 변경으로 로그 이벤트가 촉발하는 것은 원하지 않습니다.

The solution is again to split out the non-reactive code into an Effect Event: 해결책은 다시 비반응형 코드를 Effect Event로 분리하는 것입니다:

function Chat({ roomId, notificationCount }) {
const onVisit = useEffectEvent(visitedRoomId => {
logVisit(visitedRoomId, notificationCount);
});

useEffect(() => {
onVisit(roomId);
}, [roomId]); // ✅ All dependencies declared
// ...
}

You want your logic to be reactive with regards to roomId, so you read roomId inside of your Effect. However, you don’t want a change to notificationCount to log an extra visit, so you read notificationCount inside of the Effect Event. Learn more about reading the latest props and state from Effects using Effect Events. 로직이 roomId와 관련하여 반응하기를 원하므로 Effect 내부에서 roomId를 읽습니다. 그러나 notificationCount를 변경하여 추가 방문을 기록하는 것은 원하지 않으므로 Effect Event 내부에서 notificationCount를 읽습니다. Effect Event를 사용하여 Effect에서 최신 props과 state를 읽는 방법에 대해 자세히 알아보세요.

Does some reactive value change unintentionally?일부 반응형 값이 의도치 않게 변경되나요?

Sometimes, you do want your Effect to “react” to a certain value, but that value changes more often than you’d like—and might not reflect any actual change from the user’s perspective. For example, let’s say that you create an options object in the body of your component, and then read that object from inside of your Effect: Effect가 특정 값에 ‘반응’하기를 원하지만, 그 값이 원하는 것보다 더 자주 변경되어 사용자의 관점에서 실제 변경 사항을 반영하지 못할 수도 있습니다. 예를 들어, 컴포넌트 본문에 options 객체를 생성한 다음 Effect 내부에서 해당 객체를 읽는다고 가정해 보겠습니다:

function ChatRoom({ roomId }) {
// ...
const options = {
serverUrl: serverUrl,
roomId: roomId
};

useEffect(() => {
const connection = createConnection(options);
connection.connect();
// ...

This object is declared in the component body, so it’s a reactive value. When you read a reactive value like this inside an Effect, you declare it as a dependency. This ensures your Effect “reacts” to its changes: 이 객체는 컴포넌트 본문에서 선언되므로 반응형 값입니다. Effect 내에서 이와 같은 반응형 값을 읽으면 의존성으로 선언합니다. 이렇게 하면 Effect가 변경 사항에 “반응”하게 됩니다:

// ...
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...

It is important to declare it as a dependency! This ensures, for example, that if the roomId changes, your Effect will re-connect to the chat with the new options. However, there is also a problem with the code above. To see it, try typing into the input in the sandbox below, and watch what happens in the console: 의존성으로 선언하는 것이 중요합니다! 이렇게 하면 예를 들어, roomId가 변경되면 Effect가 새 options으로 채팅에 다시 연결됩니다. 하지만 위 코드에도 문제가 있습니다. 이를 확인하려면 아래 샌드박스의 input에 타이핑하고 콘솔에서 어떤 일이 발생하는지 살펴보세요:

import { useState, useEffect } from 'react';
import { createConnection } from './chat.js';

const serverUrl = 'https://localhost:1234';

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');

  // Temporarily disable the linter to demonstrate the problem
  // eslint-disable-next-line react-hooks/exhaustive-deps
  const options = {
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [options]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

In the sandbox above, the input only updates the message state variable. From the user’s perspective, this should not affect the chat connection. However, every time you update the message, your component re-renders. When your component re-renders, the code inside of it runs again from scratch. 위의 샌드박스에서 입력은 message state 변수만 업데이트합니다. 사용자 입장에서는 이것이 채팅 연결에 영향을 미치지 않아야 합니다. 하지만 message를 업데이트할 때마다 컴포넌트가 리렌더링됩니다. 컴포넌트가 리렌더링되면 그 안에 있는 코드가 처음부터 다시 실행됩니다.

A new options object is created from scratch on every re-render of the ChatRoom component. React sees that the options object is a different object from the options object created during the last render. This is why it re-synchronizes your Effect (which depends on options), and the chat re-connects as you type. ChatRoom 컴포넌트를 리렌더링할 때마다 새로운 options 객체가 처음부터 새로 생성됩니다. React는 options 객체가 마지막 렌더링 중에 생성된 options 객체와 다른 객체임을 인식합니다. 그렇기 때문에 (options에 따라 달라지는) Effect를 다시 동기화하고 사용자가 입력할 때 채팅이 다시 연결됩니다.

This problem only affects objects and functions. In JavaScript, each newly created object and function is considered distinct from all the others. It doesn’t matter that the contents inside of them may be the same! 이 문제는 객체와 함수에만 영향을 줍니다. JavaScript에서는 새로 생성된 객체와 함수가 다른 모든 객체와 구별되는 것으로 간주됩니다. 그 안의 내용이 동일할 수 있다는 것은 중요하지 않습니다!

// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };

// These are two different objects!
console.log(Object.is(options1, options2)); // false

Object and function dependencies can make your Effect re-synchronize more often than you need. 객체 및 함수 의존성으로 인해 Effect가 필요 이상으로 자주 재동기화될 수 있습니다.

This is why, whenever possible, you should try to avoid objects and functions as your Effect’s dependencies. Instead, try moving them outside the component, inside the Effect, or extracting primitive values out of them. 그렇기 때문에 가능하면 객체와 함수를 Effect의 의존성으로 사용하지 않는 것이 좋습니다. 대신 컴포넌트 외부나 Effect 내부로 이동하거나 원시 값을 추출해 보세요.

Move static objects and functions outside your component정적 객체와 함수를 컴포넌트 외부로 이동

If the object does not depend on any props and state, you can move that object outside your component: 객체가 props 및 state에 의존하지 않는 경우 해당 객체를 컴포넌트 외부로 이동할 수 있습니다:

const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

This way, you prove to the linter that it’s not reactive. It can’t change as a result of a re-render, so it doesn’t need to be a dependency. Now re-rendering ChatRoom won’t cause your Effect to re-synchronize. 이렇게 하면 린터가 반응하지 않는다는 것을 증명할 수 있습니다. 리렌더링의 결과로 변경될 수 없으므로 의존성이 될 필요가 없습니다. 이제 ChatRoom을 리렌더링해도 Effect가 다시 동기화되지 않습니다.

This works for functions too: 이는 함수에도 적용됩니다:

function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}

function ChatRoom() {
const [message, setMessage] = useState('');

useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...

Since createOptions is declared outside your component, it’s not a reactive value. This is why it doesn’t need to be specified in your Effect’s dependencies, and why it won’t ever cause your Effect to re-synchronize. createOptions는 컴포넌트 외부에서 선언되므로 반응형 값이 아닙니다. 그렇기 때문에 Effect의 의존성에 지정할 필요가 없으며, Effect가 다시 동기화되지 않는 이유이기도 합니다.

Move dynamic objects and functions inside your EffectEffect 내에서 동적 객체 및 함수 이동

If your object depends on some reactive value that may change as a result of a re-render, like a roomId prop, you can’t pull it outside your component. You can, however, move its creation inside of your Effect’s code: 객체가 roomId 프로퍼티처럼 리렌더링의 결과로 변경될 수 있는 반응형 값에 의존하는 경우, 컴포넌트 외부로 끌어낼 수 없습니다. 하지만 Effect의 코드 내부로 이동시킬 수는 있습니다:

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]); // ✅ All dependencies declared
// ...

Now that options is declared inside of your Effect, it is no longer a dependency of your Effect. Instead, the only reactive value used by your Effect is roomId. Since roomId is not an object or function, you can be sure that it won’t be unintentionally different. In JavaScript, numbers and strings are compared by their content: 이제 options이 Effect 내부에서 선언되었으므로 더 이상 Effect의 의존성이 아닙니다. 대신 Effect에서 사용하는 유일한 반응형 값은 roomId입니다. roomId는 객체나 함수가 아니기 때문에 의도치 않게 달라지지 않을 것이라고 확신할 수 있습니다. JavaScript에서 숫자와 문자열은 그 내용에 따라 비교됩니다:

// During the first render
const roomId1 = 'music';

// During the next render
const roomId2 = 'music';

// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true

Thanks to this fix, the chat no longer re-connects if you edit the input: 이 수정 덕분에 입력을 수정해도 더 이상 채팅이 다시 연결되지 않습니다:

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)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  return (
    <>
      <label>
        Choose the chat room:{' '}
        <select
          value={roomId}
          onChange={e => setRoomId(e.target.value)}
        >
          <option value="general">general</option>
          <option value="travel">travel</option>
          <option value="music">music</option>
        </select>
      </label>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

However, it does re-connect when you change the roomId dropdown, as you would expect. 그러나 예상대로 roomId 드롭다운을 변경하면 다시 연결됩니다.

This works for functions, too: 이는 함수에서도 마찬가지입니다:

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]); // ✅ All dependencies declared
// ...

You can write your own functions to group pieces of logic inside your Effect. As long as you also declare them inside your Effect, they’re not reactive values, and so they don’t need to be dependencies of your Effect. Effect 내에서 로직을 그룹화하기 위해 자신만의 함수를 작성할 수 있습니다. Effect 내부에서 선언하는 한, 반응형 값이 아니므로 Effect의 의존성이 될 필요가 없습니다.

Read primitive values from objects객체에서 원시 값 읽기

Sometimes, you may receive an object from props: 가끔 props에서 객체를 받을 수도 있습니다:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...

The risk here is that the parent component will create the object during rendering: 렌더링 중에 부모 컴포넌트가 객체를 생성한다는 점이 위험합니다:

<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>

This would cause your Effect to re-connect every time the parent component re-renders. To fix this, read information from the object outside the Effect, and avoid having object and function dependencies: 이렇게 하면 부모 컴포넌트가 리렌더링할 때마다 Effect가 다시 연결됩니다. 이 문제를 해결하려면 Effect 외부의 객체에서 정보를 읽고 객체 및 함수 의존성을 피하십시오:

function ChatRoom({ options }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...

The logic gets a little repetitive (you read some values from an object outside an Effect, and then create an object with the same values inside the Effect). But it makes it very explicit what information your Effect actually depends on. If an object is re-created unintentionally by the parent component, the chat would not re-connect. However, if options.roomId or options.serverUrl really are different, the chat would re-connect. 로직은 약간 반복적입니다 (Effect 외부의 객체에서 일부 값을 읽은 다음 Effect 내부에 동일한 값을 가진 객체를 만듭니다). 하지만 Effect가 실제로 어떤 정보에 의존하는지 매우 명확하게 알 수 있습니다. 부모 컴포넌트에 의해 의도치 않게 객체가 다시 생성된 경우 채팅이 다시 연결되지 않습니다. 하지만 options.roomId 또는 options.serverUrl이 실제로 다른 경우 채팅이 다시 연결됩니다.

Calculate primitive values from functions함수에서 원시값 계산

The same approach can work for functions. For example, suppose the parent component passes a function: 함수에 대해서도 동일한 접근 방식을 사용할 수 있습니다. 예를 들어, 부모 컴포넌트가 함수를 전달한다고 가정해 보겠습니다:

<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>

To avoid making it a dependency (and causing it to re-connect on re-renders), call it outside the Effect. This gives you the roomId and serverUrl values that aren’t objects, and that you can read from inside your Effect: 의존성을 만들지 않으려면 (그리고 리렌더링할 때 다시 연결되는 것을 방지하려면) Effect 외부에서 호출하세요. 이렇게 하면 객체가 아니며 Effect 내부에서 읽을 수 있는 roomIdserverUrl 값을 얻을 수 있습니다:

function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');

const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...

This only works for pure functions because they are safe to call during rendering. If your function is an event handler, but you don’t want its changes to re-synchronize your Effect, wrap it into an Effect Event instead. 이는 렌더링 중에 호출해도 안전하므로 순수 함수에서만 작동합니다. 함수가 이벤트 핸들러이지만 변경 사항으로 인해 Effect가 다시 동기화되는 것을 원하지 않는 경우, 대신 Effect Event로 함수를 감싸세요.

Recap요약

  • Dependencies should always match the code.
  • When you’re not happy with your dependencies, what you need to edit is the code.
  • Suppressing the linter leads to very confusing bugs, and you should always avoid it.
  • To remove a dependency, you need to “prove” to the linter that it’s not necessary.
  • If some code should run in response to a specific interaction, move that code to an event handler.
  • If different parts of your Effect should re-run for different reasons, split it into several Effects.
  • If you want to update some state based on the previous state, pass an updater function.
  • If you want to read the latest value without “reacting” it, extract an Effect Event from your Effect.
  • In JavaScript, objects and functions are considered different if they were created at different times.
  • Try to avoid object and function dependencies. Move them outside the component or inside the Effect.
  • 의존성은 항상 코드와 일치해야 합니다.
  • 의존성이 마음에 들지 않으면 코드를 수정해야 합니다.
  • 린터를 억제하면 매우 혼란스러운 버그가 발생하므로 항상 피해야 합니다.
  • 의존성을 제거하려면 해당 의존성이 필요하지 않다는 것을 린터에게 “증명”해야 합니다.
  • 특정 상호작용에 대한 응답으로 일부 코드가 실행되어야 하는 경우 해당 코드를 이벤트 핸들러로 이동하세요.
  • Effect의 다른 부분이 다른 이유로 다시 실행되어야 하는 경우 여러 개의 Effect로 분할하세요.
  • 이전 state를 기반으로 일부 state를 업데이트하려면 업데이터 함수를 전달하세요.
  • ”반응”하지 않고 최신 값을 읽으려면 Effect에서 Effect Event를 추출하세요.
  • JavaScript에서 객체와 함수는 서로 다른 시간에 생성된 경우 서로 다른 것으로 간주됩니다.
  • 객체와 함수의 의존성을 피하세요. 컴포넌트 외부나 Effect 내부로 이동하세요.

Challenge 1 of 4: Fix a resetting intervalinterval 초기화 수정하기

This Effect sets up an interval that ticks every second. You’ve noticed something strange happening: it seems like the interval gets destroyed and re-created every time it ticks. Fix the code so that the interval doesn’t get constantly re-created. 이 Effect는 매초마다 증가되는 interval을 설정합니다. 이상한 일이 발생하는 것을 발견했습니다: interval이 증가될 때마다 interval이 파괴되고 다시 생성되는 것 같습니다. interval이 계속 다시 생성되지 않도록 코드를 수정하세요.

import { useState, useEffect } from 'react';

export default function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    console.log('✅ Creating an interval');
    const id = setInterval(() => {
      console.log('⏰ Interval tick');
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('❌ Clearing an interval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>Counter: {count}</h1>
}