비공식 사이트. 24.12.31. 폐쇄예정
공식사이트 바로가기

Lifecycle of Reactive Effects반응형 Effect의 생명주기

Effects have a different lifecycle from components. Components may mount, update, or unmount. An Effect can only do two things: to start synchronizing something, and later to stop synchronizing it. This cycle can happen multiple times if your Effect depends on props and state that change over time. React provides a linter rule to check that you’ve specified your Effect’s dependencies correctly. This keeps your Effect synchronized to the latest props and state. Effect는 컴포넌트와 다른 생명주기를 가집니다. 컴포넌트는 마운트, 업데이트 또는 마운트 해제할 수 있습니다. Effect는 동기화를 시작하고 나중에 동기화를 중지하는 두 가지 작업만 할 수 있습니다. 이 사이클은 시간이 지남에 따라 변하는 props와 state에 의존하는 Effect의 경우 여러 번 발생할 수 있습니다. React는 Effect의 의존성을 올바르게 지정했는지 확인하는 린터 규칙을 제공합니다. 이렇게 하면 Effect가 최신 props와 state에 동기화됩니다.

You will learn학습 내용

  • How an Effect’s lifecycle is different from a component’s lifecycle
  • How to think about each individual Effect in isolation
  • When your Effect needs to re-synchronize, and why
  • How your Effect’s dependencies are determined
  • What it means for a value to be reactive
  • What an empty dependency array means
  • How React verifies your dependencies are correct with a linter
  • What to do when you disagree with the linter
  • Effect의 생명주기가 컴포넌트의 생명주기와 다른 점
  • 각 개별 Effect를 분리해서 생각하는 방법
  • Effect를 다시 동기화해야 하는 시기와 그 이유
  • Effect의 의존성이 결정되는 방법
  • 값이 반응형이라는 것의 의미
  • 빈 의존성 배열이 의미하는 것
  • React가 린터로 의존성이 올바른지 확인하는 방법
  • 린터에 동의하지 않을 때 해야 할 일

The lifecycle of an EffectEffect의 생명주기

Every React component goes through the same lifecycle: 모든 React 컴포넌트는 동일한 생명주기를 거칩니다:

  • A component mounts when it’s added to the screen.
  • A component updates when it receives new props or state, usually in response to an interaction.
  • A component unmounts when it’s removed from the screen.
  • 컴포넌트는 화면에 추가될 때 마운트됩니다.
  • 컴포넌트는 새로운 props나 state를 받으면 업데이트됩니다. 이는 보통 상호작용에 대한 응답으로 발생합니다.
  • 화면에서 제거되면 컴포넌트가 마운트 해제됩니다.

It’s a good way to think about components, but not about Effects. Instead, try to think about each Effect independently from your component’s lifecycle. An Effect describes how to synchronize an external system to the current props and state. As your code changes, synchronization will need to happen more or less often. 컴포넌트에 대해 생각하는 좋은 방법이지만 Effect에 대해서는 생각하지 않는 것이 좋습니다. 대신 각 Effect를 컴포넌트의 생명주기와 독립적으로 생각해보세요. Effect는 외부 시스템을 현재 props 및 state에 동기화하는 방법을 설명합니다. 코드가 변경되면 이 동기화를 더 자주 또는 덜 자주 수행해야 합니다.

To illustrate this point, consider this Effect connecting your component to a chat server: 이 점을 설명하기 위해 컴포넌트를 채팅 서버에 연결하는 Effect를 예로 들어보겠습니다:

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

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

Your Effect’s body specifies how to start synchronizing: Effect의 본문에는 동기화 시작 방법이 명시되어 있습니다:

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

The cleanup function returned by your Effect specifies how to stop synchronizing: Effect에서 반환되는 클린업 함수는 동기화를 중지하는 방법을 지정합니다:

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

Intuitively, you might think that React would start synchronizing when your component mounts and stop synchronizing when your component unmounts. However, this is not the end of the story! Sometimes, it may also be necessary to start and stop synchronizing multiple times while the component remains mounted. 직관적으로 React는 컴포넌트가 마운트될 때 동기화를 시작하고 컴포넌트가 마운트 해제될 때 동기화를 중지할 것이라고 생각할 수 있습니다. 하지만 이것이 이야기의 끝이 아닙니다! 때로는 컴포넌트가 마운트된 상태에서 동기화를 여러 번 시작하고 중지해야 할 수도 있습니다.

Let’s look at why this is necessary, when it happens, and how you can control this behavior. 이러한 동작이 필요한 이유와 발생 시기, 그리고 이러한 동작을 제어할 수 있는 방법을 살펴보겠습니다.

Note

Some Effects don’t return a cleanup function at all. More often than not, you’ll want to return one—but if you don’t, React will behave as if you returned an empty cleanup function. 일부 Effect는 클린업 함수를 전혀 반환하지 않습니다. 대부분의 경우 함수를 반환하고 싶겠지만, 그렇지 않은 경우 React는 아무 작업도 하지 않는 빈 클린업 함수를 반환한 것처럼 동작합니다.

Why synchronization may need to happen more than once동기화가 두 번 이상 수행되어야 하는 이유

Imagine this ChatRoom component receives a roomId prop that the user picks in a dropdown. Let’s say that initially the user picks the "general" room as the roomId. Your app displays the "general" chat room: ChatRoom 컴포넌트가 사용자가 드롭다운에서 선택한 roomId prop을 받는다고 가정해 보세요. 처음에 사용자가 "general" 채팅방을 roomId로 선택했다고 가정해 봅시다. 앱에 "general" 채팅방이 표시됩니다:

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

function ChatRoom({ roomId /* "general" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

After the UI is displayed, React will run your Effect to start synchronizing. It connects to the "general" room: UI가 표시되면 React가 Effect를 실행하여 동기화를 시작합니다. "general" 방에 연결됩니다:

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
}, [roomId]);
// ...

So far, so good. 지금까지는 괜찮습니다.

Later, the user picks a different room in the dropdown (for example, "travel"). First, React will update the UI: 이후, 사용자가 드롭다운에서 다른 방을 선택합니다(예: "travel"). 먼저 React가 UI를 업데이트합니다:

function ChatRoom({ roomId /* "travel" */ }) {
// ...
return <h1>Welcome to the {roomId} room!</h1>;
}

Think about what should happen next. The user sees that "travel" is the selected chat room in the UI. However, the Effect that ran the last time is still connected to the "general" room. The roomId prop has changed, so what your Effect did back then (connecting to the "general" room) no longer matches the UI. 다음에 어떤 일이 일어날지 생각해 보세요. 사용자는 UI에서 "travel"이 선택된 대화방임을 알 수 있습니다. 하지만 지난번에 실행된 Effect는 여전히 "general"대화방에 연결되어 있습니다. roomId prop이 변경되었기 때문에 이전에 Effect("general" 방에 연결)가 수행한 작업이 더 이상 UI와 일치하지 않습니다.

At this point, you want React to do two things: 이 시점에서 React가 두 가지 작업을 수행하기를 원합니다:

  1. Stop synchronizing with the old roomId (disconnect from the "general" room)
  2. Start synchronizing with the new roomId (connect to the "travel" room)
  1. 이전 roomId와의 동기화 중지 ("general" 룸에서 연결 해제)
  2. roomId와 동기화 시작 ("travel" 룸과 연결)

Luckily, you’ve already taught React how to do both of these things! Your Effect’s body specifies how to start synchronizing, and your cleanup function specifies how to stop synchronizing. All that React needs to do now is to call them in the correct order and with the correct props and state. Let’s see how exactly that happens. 다행히 여러분은 이미 이 두 가지를 수행하는 방법을 React에 가르쳤습니다! Effect의 본문은 동기화를 시작하는 방법을 지정하고, 클린업 함수는 동기화를 중지하는 방법을 지정합니다. 이제 React가 해야 할 일은 올바른 순서로 올바른 props와 state로 호출하기만 하면 됩니다. 정확히 어떻게 일어나는지 살펴보겠습니다.

How React re-synchronizes your EffectReact가 Effect를 재동기화 하는 방법

Recall that your ChatRoom component has received a new value for its roomId prop. It used to be "general", and now it is "travel". React needs to re-synchronize your Effect to re-connect you to a different room. ChatRoom컴포넌트의 roomId prop이 새로운 값을 받았다는 것을 기억하세요. 이전에는 "general"이었지만 이제는 "travel"입니다. 다른 방에 다시 연결하려면 React가 Effect를 다시 동기화해야 합니다.

To stop synchronizing, React will call the cleanup function that your Effect returned after connecting to the "general" room. Since roomId was "general", the cleanup function disconnects from the "general" room: 동기화를 중지하기 위해, React는 "general" 방에 연결한 후 Effect가 반환한 클린업 함수를 호출합니다. roomId"general"이므로, 클린업 함수는 "general" 방에서 연결을 끊습니다:

function ChatRoom({ roomId /* "general" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "general" room
connection.connect();
return () => {
connection.disconnect(); // Disconnects from the "general" room
};
// ...

Then React will run the Effect that you’ve provided during this render. This time, roomId is "travel" so it will start synchronizing to the "travel" chat room (until its cleanup function is eventually called too): 그러면 React는 이 렌더링 중에 여러분이 제공한 Effect를 실행합니다. 이번에는 roomId"travel"이므로 (클린업 함수가 호출되기 전까지) "travel"채팅방과 동기화되기 시작합니다:

function ChatRoom({ roomId /* "travel" */ }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Connects to the "travel" room
connection.connect();
// ...

Thanks to this, you’re now connected to the same room that the user chose in the UI. Disaster averted! 덕분에 이제 사용자가 UI에서 선택한 방과 동일한 방에 연결됩니다. 재앙을 피했습니다!

Every time after your component re-renders with a different roomId, your Effect will re-synchronize. For example, let’s say the user changes roomId from "travel" to "music". React will again stop synchronizing your Effect by calling its cleanup function (disconnecting you from the "travel" room). Then it will start synchronizing again by running its body with the new roomId prop (connecting you to the "music" room). 컴포넌트가 다른 roomId로 다시 렌더링할 때마다 Effect가 다시 동기화됩니다. 예를 들어, 사용자가 roomId"travel"에서 "music"으로 변경한다고 가정해 봅시다. React는 다시 클린업 함수를 호출하여 Effect 동기화를 중지합니다("travel" 방에서 연결을 끊습니다). 그런 다음 새 roomId prop으로 본문을 실행하여 다시 동기화를 시작합니다("music" 방에 연결).

Finally, when the user goes to a different screen, ChatRoom unmounts. Now there is no need to stay connected at all. React will stop synchronizing your Effect one last time and disconnect you from the "music" chat room. 마지막으로 사용자가 다른 화면으로 이동하면 ChatRoom이 마운트 해제됩니다. 이제 연결 상태를 유지할 필요가 전혀 없습니다. React는 마지막으로 Effect의 동기화를 중지하고 "music" 채팅방에서 연결을 끊습니다.

Thinking from the Effect’s perspective Effect의 관점에서 생각하기

Let’s recap everything that’s happened from the ChatRoom component’s perspective: ChatRoom 컴포넌트의 관점에서 일어난 모든 일을 요약해 보겠습니다:

  1. ChatRoom mounted with roomId set to "general"
  2. ChatRoom updated with roomId set to "travel"
  3. ChatRoom updated with roomId set to "music"
  4. ChatRoom unmounted
  1. roomId"general"로 설정된 상태로 ChatRoom이 마운트됨
  2. roomId"travel"로 설정된 상태로 ChatRoom이 업데이트됨
  3. roomId"music"로 설정된 상태로 ChatRoom이 업데이트됨
  4. ChatRoom 마운트 해제됨

During each of these points in the component’s lifecycle, your Effect did different things: 컴포넌트 생명주기의 각 시점에서 Effect는 서로 다른 작업을 수행했습니다:

  1. Your Effect connected to the "general" room
  2. Your Effect disconnected from the "general" room and connected to the "travel" room
  3. Your Effect disconnected from the "travel" room and connected to the "music" room
  4. Your Effect disconnected from the "music" room
  1. Effect가 "general" 방에 연결됨
  2. Effect가 "general" 방과의 연결이 끊어지고 "travel" 방에 연결됨
  3. Effect가 "travel" 방과의 연결이 끊어지고 "music" 방에 연결됨
  4. Effect가 "music" 방과의 연결이 끊어짐

Now let’s think about what happened from the perspective of the Effect itself: 이제 Effect 자체의 관점에서 무슨 일이 일어났는지 생각해 봅시다:

useEffect(() => {
// Your Effect connected to the room specified with roomId...
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
// ...until it disconnected
connection.disconnect();
};
}, [roomId]);

This code’s structure might inspire you to see what happened as a sequence of non-overlapping time periods: 이 코드의 구조는 어떤 일이 일어났는지 겹치지 않는 기간의 연속으로 보는 데 영감을 줄 수 있습니다:

  1. Your Effect connected to the "general" room (until it disconnected)
  2. Your Effect connected to the "travel" room (until it disconnected)
  3. Your Effect connected to the "music" room (until it disconnected)
  1. Effect가 "general" 방에 연결됨 (연결이 끊어질 때까지)
  2. Effect가 "travel" 방에 연결됨 (연결이 끊어질 때까지)
  3. Effect가 "music" 방에 연결됨 (연결이 끊어질 때까지)

Previously, you were thinking from the component’s perspective. When you looked from the component’s perspective, it was tempting to think of Effects as “callbacks” or “lifecycle events” that fire at a specific time like “after a render” or “before unmount”. This way of thinking gets complicated very fast, so it’s best to avoid. 이전에는 컴포넌트의 관점에서 생각했습니다. 컴포넌트의 관점에서 보면 Effect를 “렌더링 후” 또는 “마운트 해제 전”과 같은 특정 시점에 실행되는 “콜백” 또는 “생명주기 이벤트”로 생각하기 쉬웠습니다. 이러한 사고 방식은 매우 빠르게 복잡해지므로 피하는 것이 가장 좋습니다.

Instead, always focus on a single start/stop cycle at a time. It shouldn’t matter whether a component is mounting, updating, or unmounting. All you need to do is to describe how to start synchronization and how to stop it. If you do it well, your Effect will be resilient to being started and stopped as many times as it’s needed. 대신 항상 한 번에 하나의 시작/중지 사이클에만 집중하세요. 컴포넌트를 마운트, 업데이트 또는 마운트 해제하는 것은 중요하지 않습니다. 동기화를 시작하는 방법과 중지하는 방법만 설명하면 됩니다. 이 작업을 잘 수행하면 필요한 횟수만큼 Effect를 시작하고 중지할 수 있는 탄력성을 확보할 수 있습니다.

This might remind you how you don’t think whether a component is mounting or updating when you write the rendering logic that creates JSX. You describe what should be on the screen, and React figures out the rest. JSX를 생성하는 렌더링 로직을 작성할 때 컴포넌트가 마운트되는지 업데이트되는지 생각하지 않는 것을 떠올리면 이해가 쉬울 것입니다. 화면에 무엇이 표시되어야 하는지 설명하면 나머지는 React가 알아서 처리합니다.

How React verifies that your Effect can re-synchronizeReact가 Effect의 재동기화 가능 여부를 확인하는 방법

Here is a live example that you can play with. Press “Open chat” to mount the ChatRoom component: 다음은 실제 사용 가능한 예제입니다. “Open chat”을 눌러 ChatRoom 컴포넌트를 마운트 해보세요:

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');
  const [show, setShow] = useState(false);
  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>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}

Notice that when the component mounts for the first time, you see three logs: 컴포넌트가 처음 마운트될 때 3개의 로그가 표시됩니다:

  1. ✅ Connecting to "general" room at https://localhost:1234... (development-only)
  2. ❌ Disconnected from "general" room at https://localhost:1234. (development-only)
  3. ✅ Connecting to "general" room at https://localhost:1234...

The first two logs are development-only. In development, React always remounts each component once. 처음 두 개의 로그는 개발 모드 전용입니다. 개발 모드에서 React는 항상 각 컴포넌트를 한 번씩 다시 마운트합니다.

React verifies that your Effect can re-synchronize by forcing it to do that immediately in development. This might remind you of opening a door and closing it an extra time to check if the door lock works. React starts and stops your Effect one extra time in development to check you’ve implemented its cleanup well.

개발 모드에서 React는 즉시 강제로 동기화를 수행하여 Effect가 다시 동기화될 수 있는지 확인합니다. 도어락이 작동하는지 확인하기 위해 문을 열었다가 한 번 더 닫는 것과 비슷합니다. React는 개발 중에 Effect를 한 번 더 시작하고 중지하여 클린업 함수를 잘 구현했는지 확인합니다.

The main reason your Effect will re-synchronize in practice is if some data it uses has changed. In the sandbox above, change the selected chat room. Notice how, when the roomId changes, your Effect re-synchronizes. 실제로 Effect가 다시 동기화되는 주된 이유는 Effect가 사용하는 일부 데이터가 변경된 경우입니다. 위의 샌드박스에서 선택한 채팅방을 변경해 보세요. roomId가 변경되면 Effect가 다시 동기화되는 것을 확인할 수 있습니다.

However, there are also more unusual cases in which re-synchronization is necessary. For example, try editing the serverUrl in the sandbox above while the chat is open. Notice how the Effect re-synchronizes in response to your edits to the code. In the future, React may add more features that rely on re-synchronization. 그러나 재동기화가 필요한 더 특이한 경우도 있습니다. 예를 들어, 채팅이 열려 있는 상태에서 위의 샌드박스에서 serverUrl을 편집해 보세요. 코드 편집에 대한 응답으로 Effect가 어떻게 다시 동기화되는지 주목하세요. 앞으로 React는 재동기화에 의존하는 더 많은 기능을 추가할 수 있습니다.

How React knows that it needs to re-synchronize the EffectReact가 Effect의 재동기화 필요성을 인식하는 방법

You might be wondering how React knew that your Effect needed to re-synchronize after roomId changes. It’s because you told React that its code depends on roomId by including it in the list of dependencies: roomId가 변경되었을 때 React가 Effect를 다시 동기화해야 한다는 것을 어떻게 알았는지 궁금할 것입니다. 그 이유는 의존성 목록roomId를 포함시킴으로써 코드가 roomId에 의존하고 있음을 React에 알렸기 때문입니다:

function ChatRoom({ roomId }) { // The roomId prop may change over time
// roomId prop은 시간에 따라 바뀔 수 있음
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // This Effect reads roomId
// 이 Effect는 roomId를 읽음
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]); // So you tell React that this Effect "depends on" roomId
// 이 Effect가 roomId에 의존함을 React에 알림.
// ...

Here’s how this works: 작동 방식은 다음과 같습니다:

  1. You knew roomId is a prop, which means it can change over time.
  2. You knew that your Effect reads roomId (so its logic depends on a value that may change later).
  3. This is why you specified it as your Effect’s dependency (so that it re-synchronizes when roomId changes).
  1. roomId는 prop이므로 시간이 지남에 따라 변경될 수 있다는 것을 알고 있습니다.
  2. Effect가 roomId를 읽는다는 것을 알았습니다 (따라서 해당 로직은 나중에 변경될 수 있는 값에 따라 달라집니다).
  3. 이 때문에 Effect의 의존성으로 지정했습니다(roomId가 변경될 때 다시 동기화되도록).

Every time after your component re-renders, React will look at the array of dependencies that you have passed. If any of the values in the array is different from the value at the same spot that you passed during the previous render, React will re-synchronize your Effect. 컴포넌트가 다시 렌더링될 때마다 React는 사용자가 전달한 의존성 배열을 살펴봅니다. 배열의 값 중 하나라도 이전 렌더링 중에 전달한 동일한 지점의 값과 다르면 React는 Effect를 다시 동기화합니다.

For example, if you passed ["general"] during the initial render, and later you passed ["travel"] during the next render, React will compare "general" and "travel". These are different values (compared with Object.is), so React will re-synchronize your Effect. On the other hand, if your component re-renders but roomId has not changed, your Effect will remain connected to the same room. 예를 들어, 초기 렌더링 중에 ["general"]을 전달했고 나중에 다음 렌더링 중에 ["travel"]을 전달한 경우, React는 "general""travel"을 비교합니다. 이 값은 (Object.is와 비교했을 때) 다른 값이기 때문에 React는 Effect를 다시 동기화할 것입니다. 반면 컴포넌트가 다시 렌더링될 때 roomId가 변경되어있지 않은 경우 Effect는 동일한 방에 연결된 상태로 유지됩니다.

Each Effect represents a separate synchronization process각각의 Effect는 별도의 동기화 프로세스를 나타냅니다.

Resist adding unrelated logic to your Effect only because this logic needs to run at the same time as an Effect you already wrote. For example, let’s say you want to send an analytics event when the user visits the room. You already have an Effect that depends on roomId, so you might feel tempted to add the analytics call there: 이 로직은 이미 작성한 Effect와 동시에 실행되어야 하므로 관련 없는 로직을 Effect에 추가하지 마세요. 예를 들어, 사용자가 방을 방문할 때 분석 이벤트를 전송하고 싶다고 가정해 봅시다. 이미 roomId에 의존하는 Effect가 있으므로 바로 그 Effect에서 분석 이벤트 호출을 추가하고 싶을 수 있습니다:

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

But imagine you later add another dependency to this Effect that needs to re-establish the connection. If this Effect re-synchronizes, it will also call logVisit(roomId) for the same room, which you did not intend. Logging the visit is a separate process from connecting. Write them as two separate Effects: 하지만 나중에 이 Effect에 연결을 다시 설정해야 하는 다른 의존성을 추가한다고 가정해 보겠습니다. 이 Effect가 다시 동기화되면 의도하지 않은 동일한 방에 대해 logVisit(roomId)도 호출하게 됩니다. 방문을 기록하는 것은 연결과는 별개의 프로세스입니다. 그렇기 때문에 두 개의 개별 Effect로 작성해야 합니다:

function ChatRoom({ roomId }) {
useEffect(() => {
logVisit(roomId);
}, [roomId]);

useEffect(() => {
const connection = createConnection(serverUrl, roomId);
// ...
}, [roomId]);
// ...
}

Each Effect in your code should represent a separate and independent synchronization process. 코드의 각 Effect는 별도의 독립적인 동기화 프로세스를 나타내야 합니다.

In the above example, deleting one Effect wouldn’t break the other Effect’s logic. This is a good indication that they synchronize different things, and so it made sense to split them up. On the other hand, if you split up a cohesive piece of logic into separate Effects, the code may look “cleaner” but will be more difficult to maintain. This is why you should think whether the processes are same or separate, not whether the code looks cleaner. 위의 예시에서는 한 Effect를 삭제해도 다른 Effect의 로직이 깨지지 않습니다. 이는 서로 다른 것을 동기화하므로 분리하는 것이 합리적이라는 것을 나타냅니다. 반면 일관된 로직을 별도의 Effect로 분리하면 코드가 “더 깔끔해” 보일 수 있지만 유지 관리가 더 어려워집니다. 따라서 코드가 더 깔끔해 보이는지 여부가 아니라 프로세스가 동일한지 또는 분리되어 있는지를 고려해야 합니다.

Effects “react” to reactive valuesEffect는 ‘반응형 값’에 반응합니다

Your Effect reads two variables (serverUrl and roomId), but you only specified roomId as a dependency: 다음 코드에서 Effect는 두 개의 변수(serverUrlroomId)를 읽지만, 의존성에는 오직 roomId만 지정했습니다:

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

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

Why doesn’t serverUrl need to be a dependency? serverUrl을 의존성에 지정할 필요가 없는 이유가 무엇일까요?

This is because the serverUrl never changes due to a re-render. It’s always the same no matter how many times the component re-renders and why. Since serverUrl never changes, it wouldn’t make sense to specify it as a dependency. After all, dependencies only do something when they change over time! 이는 리렌더링으로 인해 serverUrl 이 변경되지 않기 때문입니다. 컴포넌트가 어떤 이유로 몇 번이나 다시 렌더링하든 항상 동일합니다. serverUrl 은 절대 변하지 않으므로 의존성으로 지정하는 것은 의미가 없습니다. 결국, 의존성은 시간이 지남에 따라 변경될 때만 무언가를 수행합니다!

On the other hand, roomId may be different on a re-render. Props, state, and other values declared inside the component are reactive because they’re calculated during rendering and participate in the React data flow. 반면 roomId는 다시 렌더링할 때 달라질 수 있습니다. 컴포넌트 내부에서 선언된 props, state 및 기타 값은 렌더링 중에 계산되고 React 데이터 흐름에 참여하기 때문에 반응형입니다.

If serverUrl was a state variable, it would be reactive. Reactive values must be included in dependencies: 만약 serverUrl이 state 변수라면, 반응형일 것입니다. 반응형 값은 의존성에 포함되어야 합니다:

function ChatRoom({ roomId }) { // Props change over time
// props는 시간에 따라 바뀜
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State may change over time
// state는 바뀔 수 있음

useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads props and state
// Effect는 props와 state를 읽음
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So you tell React that this Effect "depends on" on props and state
// 이 Effect가 props 및 state에 의존함을 React에 알림
// ...
}

By including serverUrl as a dependency, you ensure that the Effect re-synchronizes after it changes. serverUrl을 의존성으로 포함하면 Effect가 변경된 후 다시 동기화되도록 할 수 있습니다.

Try changing the selected chat room or edit the server URL in this sandbox: 이 샌드박스에서 선택한 대화방을 변경하거나 서버 URL을 수정해 보세요:

import { useState, 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();
  }, [roomId, serverUrl]);

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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} />
    </>
  );
}

Whenever you change a reactive value like roomId or serverUrl, the Effect re-connects to the chat server. roomId 또는 serverUrl과 같은 반응형 값을 변경할 때마다 Effect가 채팅 서버에 다시 연결됩니다.

What an Effect with empty dependencies means 빈 의존성을 가지고 있는 Effect의 의미

What happens if you move both serverUrl and roomId outside the component? serverUrlroomId를 모두 컴포넌트 외부로 이동하면 어떻게 되나요?

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

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

Now your Effect’s code does not use any reactive values, so its dependencies can be empty ([]). 이제 Effect의 코드는 반응형 값을 사용하지 않으므로 의존성이 비어 있을 수 있습니다([]).

Thinking from the component’s perspective, the empty [] dependency array means this Effect connects to the chat room only when the component mounts, and disconnects only when the component unmounts. (Keep in mind that React would still re-synchronize it an extra time in development to stress-test your logic.) 컴포넌트의 관점에서 생각해보면, 빈 [] 의존성 배열은 이 Effect가 컴포넌트가 마운트될 때만 채팅방에 연결되고 컴포넌트가 마운트 해제될 때만 연결이 끊어진다는 것을 의미합니다. (React는 개발 모드에서 로직을 테스트하기 위해 한 번 더 동기화한다는 점을 기억하세요).

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

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

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

export default function App() {
  const [show, setShow] = useState(false);
  return (
    <>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom />}
    </>
  );
}

However, if you think from the Effect’s perspective, you don’t need to think about mounting and unmounting at all. What’s important is you’ve specified what your Effect does to start and stop synchronizing. Today, it has no reactive dependencies. But if you ever want the user to change roomId or serverUrl over time (and they would become reactive), your Effect’s code won’t change. You will only need to add them to the dependencies. 하지만 Effect의 관점에서 생각하면 마운트 및 마운트 해제에 대해 전혀 생각할 필요가 없습니다. 중요한 것은 Effect가 동기화를 시작하고 중지하는 작업을 지정한 것입니다. 현재는 반응형 의존성이 없습니다. 하지만 사용자가 시간이 지남에 따라 roomId 또는 serverUrl을 변경하기를 원한다면(그래서 반응형이어야 한다면) Effect의 코드는 변경되지 않습니다. 의존성에 추가하기만 하면 됩니다.

All variables declared in the component body are reactive컴포넌트 본문에서 선언된 모든 변수는 반응형입니다

Props and state aren’t the only reactive values. Values that you calculate from them are also reactive. If the props or state change, your component will re-render, and the values calculated from them will also change. This is why all variables from the component body used by the Effect should be in the Effect dependency list. props와 state만 반응형 값인 것은 아닙니다. 이들로부터 계산하는 값들 역시 반응형입니다. props나 state가 변경되면 컴포넌트가 다시 렌더링되고 그로부터 계산된 값도 변경됩니다. 그렇기 때문에 Effect가 사용하는 컴포넌트 본문의 모든 변수는 Effect 의존성 목록에 있어야 합니다.

Let’s say that the user can pick a chat server in the dropdown, but they can also configure a default server in settings. Suppose you’ve already put the settings state in a context so you read the settings from that context. Now you calculate the serverUrl based on the selected server from props and the default server: 사용자가 드롭다운에서 채팅 서버를 선택할 수도 있고, 설정에서 기본 서버를 구성할 수도 있다고 가정해 봅시다. 이미 settings state를 context에 넣어서 해당 context에서 읽었다고 가정해 보겠습니다. 이제 props에서 선택한 서버와 context에서 기본 서버를 기준으로 serverUrl을 계산합니다:

function ChatRoom({ roomId, selectedServerUrl }) { // roomId is reactive
const settings = useContext(SettingsContext); // settings is reactive
const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId); // Your Effect reads roomId and serverUrl
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId, serverUrl]); // So it needs to re-synchronize when either of them changes!
// ...
}

In this example, serverUrl is not a prop or a state variable. It’s a regular variable that you calculate during rendering. But it’s calculated during rendering, so it can change due to a re-render. This is why it’s reactive. 이 예제에서 serverUrl은 prop이나 state 변수가 아닙니다. 렌더링 중에 계산하는 일반 변수입니다. 하지만 렌더링 중에 계산되므로 리렌더링으로 인해 변경될 수 있습니다. 따라서 serverUrl은 반응형 변수입니다.

All values inside the component (including props, state, and variables in your component’s body) are reactive. Any reactive value can change on a re-render, so you need to include reactive values as Effect’s dependencies. 컴포넌트 내부의 모든 값(컴포넌트 본문의 props, state, 변수 포함)은 반응형입니다. 모든 반응형 값은 다시 렌더링할 때 변경될 수 있으므로 반응형 값을 Effect의 의존성으로 포함시켜야 합니다.

In other words, Effects “react” to all values from the component body. 즉, Effect는 컴포넌트 본문의 모든 값에 “반응”합니다.

Deep Dive | 심층 탐구

Can global or mutable values be dependencies? 전역 또는 변이 가능한 값이 의존성이 될 수 있나요?

Mutable values (including global variables) aren’t reactive. 변이 가능한 값(전역 변수 포함)은 반응하지 않습니다.

A mutable value like location.pathname can’t be a dependency. It’s mutable, so it can change at any time completely outside of the React rendering data flow. Changing it wouldn’t trigger a re-render of your component. Therefore, even if you specified it in the dependencies, React wouldn’t know to re-synchronize the Effect when it changes. This also breaks the rules of React because reading mutable data during rendering (which is when you calculate the dependencies) breaks purity of rendering. Instead, you should read and subscribe to an external mutable value with useSyncExternalStore. location.pathname과 같은 변이 가능한 값은 의존성이 될 수 없습니다. 이 값은 변이 가능하므로 React 렌더링 데이터 흐름 외부에서 언제든지 바뀔 수 있습니다. 이 값을 변경해도 컴포넌트가 다시 렌더링되지 않습니다. 따라서 이를 의존성에 지정하더라도 React는 이 값이 변경될 때 Effect를 다시 동기화해야 하는지 알 수 없습니다. 또한 렌더링 도중(의존성을 계산할 때) 변경 가능한 데이터를 읽는 것은 렌더링의 순수성을 깨뜨리기 때문에 React의 규칙을 위반합니다. 대신, useSyncExternalStore를 사용하여 외부 변경 가능한 값을 읽고 구독해야 합니다.

A mutable value like ref.current or things you read from it also can’t be a dependency. The ref object returned by useRef itself can be a dependency, but its current property is intentionally mutable. It lets you keep track of something without triggering a re-render. But since changing it doesn’t trigger a re-render, it’s not a reactive value, and React won’t know to re-run your Effect when it changes. ref.current와 같이 변이 가능한 값 또는 이 값으로부터 읽은 것 역시 의존성이 될 수 없습니다. useRef가 반환하는 ref 객체 자체는 의존성이 될 수 있지만, current 프로퍼티는 의도적으로 변이 가능합니다. 이를 통해 리렌더링을 촉발하지 않고도 무언가를 추적할 수 있습니다. 하지만 이를 변경하더라도 리렌더링을 촉발하지는 않기 때문에, 이는 반응형 값이 아니며, React는 이 값이 변경될 때 Effect를 다시 실행해야 할지 알 수 없습니다.

As you’ll learn below on this page, a linter will check for these issues automatically. 이 페이지 아래에서 배우게 되겠지만, 린터는 이러한 문제를 자동으로 확인해 줍니다.

React verifies that you specified every reactive value as a dependencyReact는 모든 반응형 값을 의존성으로 지정했는지 검토합니다

If your linter is configured for React, it will check that every reactive value used by your Effect’s code is declared as its dependency. For example, this is a lint error because both roomId and serverUrl are reactive: 린터가 React에 맞게 구성된 경우, Effect 코드에서 사용되는 모든 반응형 값이 해당 의존성으로 선언되었는지 확인합니다. 예를 들어, 다음 코드는 roomIdserverUrl가 모두 반응형이므로 린트 오류입니다:

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

function ChatRoom({ roomId }) { // roomId is reactive
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // <-- Something's wrong here!

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <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} />
    </>
  );
}

This may look like a React error, but really React is pointing out a bug in your code. Both roomId and serverUrl may change over time, but you’re forgetting to re-synchronize your Effect when they change. You will remain connected to the initial roomId and serverUrl even after the user picks different values in the UI. 이것은 React 오류처럼 보일 수 있지만 실제로는 코드의 버그를 지적하는 것입니다. roomIdserverUrl은 시간이 지남에 따라 변경될 수 있지만, 변경 시 Effect를 다시 동기화하는 것을 잊어버리고 있습니다. 결과적으로 사용자가 UI에서 다른 값을 선택한 후에도 초기 roomIdserverUrl에 연결된 상태로 유지됩니다.

To fix the bug, follow the linter’s suggestion to specify roomId and serverUrl as dependencies of your Effect: 버그를 수정하려면 린터의 제안에 따라 Effect의 의존성 요소로 roomIdserverUrl을 지정하세요:

function ChatRoom({ roomId }) { // roomId is reactive
const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // serverUrl is reactive
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [serverUrl, roomId]); // ✅ All dependencies declared
// ...
}

Try this fix in the sandbox above. Verify that the linter error is gone, and the chat re-connects when needed. 위의 샌드박스에서 이 수정 방법을 시도해 보세요. 지연 오류가 사라지고 필요할 때 채팅이 다시 연결되는지 확인하세요.

Note

In some cases, React knows that a value never changes even though it’s declared inside the component. For example, the set function returned from useState and the ref object returned by useRef are stable—they are guaranteed to not change on a re-render. Stable values aren’t reactive, so you may omit them from the list. Including them is allowed: they won’t change, so it doesn’t matter. 어떤 경우에는 컴포넌트 내부에서 값이 선언되더라도 절대 변하지 않는다는 것을 React가 알고 있습니다. 예를 들어, useState에서 반환된 설정자 함수useRef에서 반환된 ref 객체는 리렌더링 시 변경되지 않도록 보장되는 안정적인 값입니다. 안정적인 값은 반응하지 않으므로 린터를 사용하면 목록에서 생략할 수 있습니다. 그러나 이러한 값을 포함하는 것은 허용됩니다. 변경되지 않으므로 상관없습니다.

What to do when you don’t want to re-synchronize 재동기화를 원치 않는 경우엔 어떻게 해야 하나요?

In the previous example, you’ve fixed the lint error by listing roomId and serverUrl as dependencies. 이전 예제에서는 roomIdserverUrl 를 의존성으로 나열하여 린트 오류를 수정했습니다.

However, you could instead “prove” to the linter that these values aren’t reactive values, i.e. that they can’t change as a result of a re-render. For example, if serverUrl and roomId don’t depend on rendering and always have the same values, you can move them outside the component. Now they don’t need to be dependencies: 그러나 대신 이러한 값이 반응형 값이 아니라는 것, 즉,리렌더링의 결과로 변경될 수 없다는 것을 린터에 “증명”할 수 있습니다. 예를 들어, serverUrlroomId가 렌더링에 의존하지 않고 항상 같은 값을 갖는다면 컴포넌트 외부로 옮길 수 있습니다. 이제 의존성이 될 필요가 없습니다:

const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive

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

You can also move them inside the Effect. They aren’t calculated during rendering, so they’re not reactive: 또한 Effect 내부로 이동할 수도 있습니다. 렌더링 중에 계산되지 않으므로 반응하지 않습니다:

function ChatRoom() {
useEffect(() => {
const serverUrl = 'https://localhost:1234'; // serverUrl is not reactive
const roomId = 'general'; // roomId is not reactive
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, []); // ✅ All dependencies declared
// ...
}

Effects are reactive blocks of code. They re-synchronize when the values you read inside of them change. Unlike event handlers, which only run once per interaction, Effects run whenever synchronization is necessary. Effect는 반응형 코드 블록입니다. 내부에서 읽은 값이 변경되면 다시 동기화됩니다. 상호작용당 한 번만 실행되는 이벤트 핸들러와 달리 Effect는 동기화가 필요할 때마다 실행됩니다.

You can’t “choose” your dependencies. Your dependencies must include every reactive value you read in the Effect. The linter enforces this. Sometimes this may lead to problems like infinite loops and to your Effect re-synchronizing too often. Don’t fix these problems by suppressing the linter! Here’s what to try instead: 의존성을 “선택”할 수는 없습니다. 의존성에는 Effect에서 읽은 모든 반응형 값이 포함되어야 합니다. 린터가 이를 강제합니다. 때때로 이로 인해 무한 루프와 같은 문제가 발생하거나 Effect가 너무 자주 다시 동기화될 수 있습니다. 린터를 억제하여 이러한 문제를 해결하지 마세요! 대신 시도할 수 있는 방법은 다음과 같습니다:

  • Check that your Effect represents an independent synchronization process. If your Effect doesn’t synchronize anything, it might be unnecessary. If it synchronizes several independent things, split it up. Effect가 독립적인 동기화 프로세스를 나타내는지 확인하세요. Effect가 아무것도 동기화하지 않는다면 불필요한 것일 수 있습니다. 여러 개의 독립적인 것을 동기화하는 경우 분할하세요.

  • If you want to read the latest value of props or state without “reacting” to it and re-synchronizing the Effect, you can split your Effect into a reactive part (which you’ll keep in the Effect) and a non-reactive part (which you’ll extract into something called an Effect Event). Read about separating Events from Effects. ‘반응’하지 않고 Effect를 재동기화하지 않으면서 props나 state의 최신 값을 읽으려면, Effect를 반응하는 부분(Effect에 유지)과 반응하지 않는 부분(Effect Event라는 것으로 추출)으로 분리할 수 있습니다. 이벤트와 Effect를 분리하는 방법에 대해 읽어보세요.

  • Avoid relying on objects and functions as dependencies. If you create objects and functions during rendering and then read them from an Effect, they will be different on every render. This will cause your Effect to re-synchronize every time. Read more about removing unnecessary dependencies from Effects. 객체와 함수를 의존성으로 사용하지 마세요. 렌더링 중에 오브젝트와 함수를 생성한 다음 Effect에서 읽으면 렌더링할 때마다 오브젝트와 함수가 달라집니다. 그러면 매번 Effect를 다시 동기화해야 합니다. Effect에서 불필요한 의존성을 제거하는 방법에 대해 읽어보세요.

Pitfall | 함정

The linter is your friend, but its powers are limited. The linter only knows when the dependencies are wrong. It doesn’t know the best way to solve each case. If the linter suggests a dependency, but adding it causes a loop, it doesn’t mean the linter should be ignored. You need to change the code inside (or outside) the Effect so that that value isn’t reactive and doesn’t need to be a dependency. 린터는 여러분의 친구이지만 그 힘은 제한되어 있습니다. 린터는 의존성이 잘못되었을 때만 알 수 있습니다. 각 사례를 해결하는 최선의 방법은 알지 못합니다. 만약 린터가 의존성을 제안하지만 이를 추가하면 루프가 발생한다고 해서 린터를 무시해야 한다는 의미는 아닙니다. 해당 값이 반응적이지 않고 의존성이 될 필요가 없도록 Effect 내부(또는 외부)의 코드를 변경해야 한다는 뜻입니다.

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
}, []);

On the next pages, you’ll learn how to fix this code without breaking the rules. It’s always worth fixing! 다음 페이지들에서는 규칙을 위반하지 않고 이 코드를 수정하는 방법을 알아보세요. 언제나 고칠 가치가 있습니다!

Recap요약

  • Components can mount, update, and unmount.
  • Each Effect has a separate lifecycle from the surrounding component.
  • Each Effect describes a separate synchronization process that can start and stop.
  • When you write and read Effects, think from each individual Effect’s perspective (how to start and stop synchronization) rather than from the component’s perspective (how it mounts, updates, or unmounts).
  • Values declared inside the component body are “reactive”.
  • Reactive values should re-synchronize the Effect because they can change over time.
  • The linter verifies that all reactive values used inside the Effect are specified as dependencies.
  • All errors flagged by the linter are legitimate. There’s always a way to fix the code to not break the rules.
  • 컴포넌트는 마운트, 업데이트, 마운트 해제할 수 있습니다.
  • 각 Effect는 주변 컴포넌트와 별도의 생명주기를 가집니다.
  • 각 Effect는 시작중지 할 수 있는 별도의 동기화 프로세스를 설명합니다.
  • Effect를 작성하고 읽을 때는 컴포넌트의 관점(마운트, 업데이트 또는 마운트 해제 방법)이 아니라 각 개별 Effect의 관점(동기화 시작 및 중지 방법)에서 생각해야 합니다.
  • 컴포넌트 본문 내부에 선언된 값은 “반응형”입니다.
  • 반응형 값은 시간이 지남에 따라 변경될 수 있으므로 Effect를 다시 동기화해야 합니다.
  • 린터는 Effect 내부에서 사용된 모든 반응형 값이 의존성으로 지정되었는지 확인합니다.
  • 린터에 의해 플래그가 지정된 모든 오류는 합법적인 오류입니다. 규칙을 위반하지 않도록 코드를 수정할 수 있는 방법은 항상 있습니다.

Challenge 1 of 5: Fix reconnecting on every keystroke키 입력시마다 재연결되는 문제 해결

In this example, the ChatRoom component connects to the chat room when the component mounts, disconnects when it unmounts, and reconnects when you select a different chat room. This behavior is correct, so you need to keep it working. 이 예제에서 ChatRoom 컴포넌트는 컴포넌트가 마운트될 때 채팅방에 연결되고, 마운트가 해제되면 연결이 끊어지며, 다른 채팅방을 선택하면 다시 연결됩니다. 이 동작은 올바른 것이므로 계속 작동하도록 해야 합니다.

However, there is a problem. Whenever you type into the message box input at the bottom, ChatRoom also reconnects to the chat. (You can notice this by clearing the console and typing into the input.) Fix the issue so that this doesn’t happen. 하지만 문제가 있습니다. 하단의 메시지 상자 입력란에 입력할 때마다 ChatRoom도 채팅에 다시 연결됩니다. (콘솔을 지우고 입력란에 입력하면 이 문제를 확인할 수 있습니다.) 이런 일이 발생하지 않도록 문제를 수정하세요.

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

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

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

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

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