ReactNextCentral

Effect: 라이프사이클

Published on
효과(Effect)는 리액트 컴포넌트의 라이프사이클과는 다르게, 데이터 동기화를 시작하고 중지하는 역할을 하는데 사용되며, 올바른 종속성 설정을 통해 최신 데이터를 유지합니다.
Table of Contents

효과(Effect)는 컴포넌트의 라이프사이클과 다른 라이프사이클을 가지고 있습니다. 컴포넌트는 마운트, 업데이트, 언마운트가 발생할 수 있지만, 효과는 두 가지 작업만 수행할 수 있습니다. 무언가를 동기화하기 시작하고, 나중에 동기화를 중지하는 것입니다. 이러한 사이클은 효과가 시간에 따라 변화하는 프롭(prop)과 상태(state)에 의존하는 경우에 여러 번 발생할 수 있습니다. 리액트는 효과의 종속성(dependencies)이 올바르게 지정되었는지 확인하기 위한 린터 규칙을 제공합니다. 이를 통해 효과가 최신의 프롭과 상태와 동기화되도록 유지할 수 있습니다.

학습 내용

  • 효과의 라이프사이클이 컴포넌트의 라이프사이클과 다른 점
  • 각 개별 효과를 개별적으로 생각하는 방법
  • 효과가 언제 다시 동기화해야 하며 그 이유
  • 효과의 종속성이 어떻게 결정되는지
  • 값이 반응적인 것을 의미하는 것
  • 빈 종속성 배열이 의미하는 것
  • 리액트가 린터를 통해 올바른 종속성을 검증하는 방법
  • 린터와의 의견 충돌 시 어떻게 해야 하는지

효과의 라이프사이클

모든 리액트 컴포넌트는 동일한 라이프사이클을 거칩니다.

  • 컴포넌트가 화면에 추가될 때 컴포넌트가 마운트됩니다.
  • 컴포넌트는 새로운 프롭이나 상태를 받아들일 때 업데이트됩니다. 일반적으로 상호작용에 응답하여 발생합니다.
  • 컴포넌트가 화면에서 제거될 때 언마운트됩니다.

이것은 컴포넌트에 대해 생각하는 좋은 방법이지만, 효과에 대해서는 그렇지 않습니다. 대신, 각 효과를 컴포넌트의 라이프사이클과는 독립적으로 생각하는 것이 좋습니다. 효과는 외부 시스템을 현재의 프롭과 상태와 동기화하는 방법을 설명합니다. 코드가 변경됨에 따라 동기화가 더 자주 또는 덜 자주 발생해야 할 것입니다.

이 점을 설명하기 위해, 컴포넌트를 채팅 서버에 연결하는 이 효과를 살펴보겠습니다.

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

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

효과의 본문은 동기화를 시작하는 방법을 지정합니다.

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

효과가 반환하는 정리(clean-up) 함수는 동기화를 중지하는 방법을 지정합니다.

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

직관적으로 생각하면 리액트가 컴포넌트가 마운트될 때 동기화를 시작하고 컴포넌트가 언마운트될 때 동기화를 중지할 것으로 예상할 수 있습니다. 하지만 이는 이야기의 끝이 아닙니다! 때로는 컴포넌트가 마운트된 상태에서 여러 번 동기화를 시작하고 중지해야 할 수도 있습니다.

이것이 왜 필요한지, 언제 발생하는지, 이 동작을 제어하는 방법에 대해 알아보겠습니다.

참고
일부 효과는 정리(clean-up) 함수를 반환하지 않습니다. 대부분의 경우, 정리 함수를 반환하는 것이 좋지만, 반환하지 않으면 리액트는 빈 정리 함수가 반환된 것처럼 동작합니다.

왜 동기화가 한 번 이상 발생해야 할 수 있는지

상상해보세요. 이 ChatRoom 컴포넌트는 사용자가 드롭다운에서 선택한 roomId prop을 받습니다. 일단 사용자가 "general" 방을 roomId로 선택한 경우를 가정해보겠습니다. 앱은 "general" 채팅방을 표시합니다.

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

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

UI가 표시된 후에 리액트는 동기화를 시작하기 위해 효과를 실행합니다. "general" 방에 연결합니다.

function ChatRoom({ roomId /* "general" */ }) {
  useEffect(() => {
    // "general" 방에 연결
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    return () => {
      // "general" 방과의 연결 해제
      connection.disconnect(); 
    };
  }, [roomId]);
  // ...

지금까지 문제 없이 진행되었습니다.

나중에 사용자가 드롭다운에서 다른 방을 선택합니다(예: "travel"). 먼저, 리액트는 UI를 업데이트합니다.

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

이제 어떤 일이 발생해야 하는지 생각해보세요. 사용자는 UI에서 "travel"이 선택된 채팅방을 볼 수 있습니다. 그러나 마지막으로 실행된 효과는 여전히 "general" 방에 연결되어 있습니다. roomId prop이 변경되었으므로 이전에 효과가 수행한 작업(즉, "general" 방에 연결)이 더 이상 UI와 일치하지 않습니다.

이 시점에서 리액트가 수행해야 할 두 가지 작업이 있습니다.

  1. 이전의 roomId와 동기화 중지하기( "general" 방과의 연결 해제)
  2. 새로운 roomId와 동기화 시작하기( "travel" 방과의 연결 시작)

다행히도, 이미 리액트에 이러한 작업을 수행하는 방법을 가르쳤습니다! 효과의 본문은 동기화 시작 방법을 지정하고, 정리 함수는 동기화 중지 방법을 지정합니다. 이제 리액트는 올바른 순서와 올바른 props 및 상태로 이들을 호출하기만 하면 됩니다. 정확히 어떻게 이루어지는지 알아보겠습니다.

리액트가 효과를 다시 동기화하는 방법

ChatRoom 컴포넌트가 새로운 roomId prop의 값으로 업데이트되었습니다. 이전 값은 "general"이었고 이제 "travel"입니다. 리액트는 효과를 다시 동기화하여 다른 방에 다시 연결해야 합니다.

동기화를 중지하기 위해 리액트는 효과가 반환한 정리 함수를 호출합니다. 이전에 "general" 방과 연결되었으므로 정리 함수는 "general" 방과의 연결을 해제합니다.

function ChatRoom({ roomId /* "general" */ }) {
  useEffect(() => {
    // "general" 방에 연결
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    return () => {
      // "general" 방과의 연결 해제
      connection.disconnect(); 
    };
    // ...

그런 다음 리액트는 이번 렌더링에서 제공한 효과를 실행합니다. 이번에는 roomId가 "travel"이므로 "travel" 채팅방에 동기화를 시작합니다(마침내 정리 함수도 호출될 때까지):

function ChatRoom({ roomId /* "travel" */ }) {
  useEffect(() => {
    // "travel" 방에 연결
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    // ...

이제 UI에서 사용자가 선택한 방과 연결되어 있습니다. 재안의 재앙이 피해갔습니다!

컴포넌트가 다른 roomId로 재렌더링된 경우마다 효과가 다시 동기화됩니다. 예를 들어, 사용자가 roomId를 "travel"에서 "music"으로 변경한다고 가정해보겠습니다. 리액트는 다시 정리 함수를 호출하여 효과의 동기화를 중지하고("travel" 방과의 연결 해제), 새로운 roomId prop과 함께 본문을 실행하여 다시 동기화를 시작합니다("music" 방에 연결).

마지막으로, 사용자가 다른 화면으로 이동하면 ChatRoom이 언마운트됩니다. 이제 연결할 필요가 없습니다. 리액트는 마지막으로 동기화를 중지하고 "music" 채팅방과의 연결을 해제합니다.

효과(Efffect)의 관점에서 생각하기

ChatRoom 컴포넌트의 관점에서 발생한 모든 일에 대해 다시 한번 되짚어 보겠습니다.

  1. ChatRoom가 마운트되었고 roomId가 "general"로 설정되었습니다.
  2. ChatRoom가 업데이트되어 roomId가 "travel"로 설정되었습니다.
  3. ChatRoom가 업데이트되어 roomId가 "music"로 설정되었습니다.
  4. ChatRoom가 언마운트되었습니다.

컴포넌트의 라이프사이클의 각 시점에서, 효과(Efffect)가 다른 동작을 수행했습니다.

  1. 효과가 "general" 방에 연결되었습니다.
  2. 효과가 "general" 방과의 연결을 해제하고 "travel" 방에 연결되었습니다.
  3. 효과가 "travel" 방과의 연결을 해제하고 "music" 방에 연결되었습니다.
  4. 효과가 "music" 방과의 연결을 해제했습니다.

이제 효과(Efffect) 자체의 관점에서 발생한 일에 대해 생각해 보겠습니다.

  useEffect(() => {
    // 효과가 `roomId`로 지정된 방에 연결했습니다...
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      // ...연결을 해제하기 전까지...
      connection.disconnect();
    };
  }, [roomId]);

이 코드의 구조를 통해 겹치지 않는 시간 단위의 연속으로 생각할 수 있습니다.

  1. 효과가 "general" 방에 연결되었습니다(해제할 때까지)
  2. 효과가 "travel" 방에 연결되었습니다(해제할 때까지)
  3. 효과가 "music" 방에 연결되었습니다(해제할 때까지)

이전에는 컴포넌트의 관점에서 생각하고 있었습니다. 컴포넌트의 관점에서 보면 효과를 "콜백"이나 "라이프사이클 이벤트"로 생각하는 것이 유혹스러웠습니다. 이러한 생각 방식은 복잡성을 증가시키므로 피하는 것이 가장 좋습니다.

대신, 항상 한 번에 하나의 시작/중지 주기에 집중하세요. 컴포넌트가 마운트, 업데이트, 언마운트되는지 여부는 중요하지 않습니다. 동기화를 시작하는 방법과 중지하는 방법만 설명하면 됩니다. 이를 잘 수행한다면, 효과(Efffect)는 필요한만큼 여러 번 시작되고 중지될 수 있는 견고성을 갖게 됩니다.

이것은 컴포넌트의 렌더링 로직을 작성할 때 컴포넌트가 마운트되거나 업데이트되는지를 고려하지 않는 방식과 유사합니다. 화면에 표시해야 할 내용을 설명하면, 리액트가 나머지 부분을 자동으로 처리합니다.

리액트가 효과(Efffect)가 다시 동기화할 수 있는지 확인하는 방법

다음은 직접 테스트할 수 있는 라이브 예제입니다. "Open chat"을 눌러 ChatRoom 컴포넌트를 마운트해 보세요:

Edit recursing-dan-brsbuh

컴포넌트가 처음 마운트될 때, 3개의 로그가 표시됩니다.

  1. ✅ "general" 방에 https://localhost:1234로 연결 중... (개발 중에만 표시됨)
  2. ❌ "general" 방에서 연결 해제됨: https://localhost:1234. (개발 중에만 표시됨)
  3. ✅ "general" 방에 https://localhost:1234로 연결 중...

첫 두 개의 로그는 개발 중에만 표시됩니다. 개발 모드에서 리액트는 항상 각 컴포넌트를 한 번 더 마운트합니다.

리액트는 효과(Efffect)가 재동기화될 수 있는지 확인하기 위해 개발 중에 즉시 그것을 강제합니다. 이는 문을 여는 것과 문 자물쇠가 작동하는지 확인하기 위해 문을 열고 한 번 더 닫는 것을 연상시킬 수 있습니다. 리액트는 개발 중에 효과(Efffect)를 한 번 더 시작하고 중지하여 정리 함수를 올바르게 구현했는지 확인합니다.

효과(Efffect)가 실제로 다시 동기화되는 주요 이유는 사용하는 일부 데이터가 변경되었을 때입니다. 위의 샌드박스에서 선택한 채팅 방을 변경해 보세요. roomId가 변경되면 효과가 다시 동기화되는 것을 확인할 수 있습니다.

그러나 다시 동기화가 필요한 더 극적인 경우도 있습니다. 예를 들어, 샌드박스에서 serverUrl을 편집해 보세요. 코딩을 편집하면 효과가 동기화되는 것을 확인할 수 있습니다. 미래에는 리액트가 다시 동기화를 의존하는 더 많은 기능을 추가할 수도 있습니다.

리액트가 효과(Efffect)를 다시 동기화해야 한다는 것을 어떻게 알까요?

roomId가 변경된 후에 리액트가 효과(Efffect)를 다시 동기화해야 한다는 것을 어떻게 알았을까요? 바로 roomId를 의존성 목록에 포함시켜 리액트에게 알려주었기 때문입니다.

// roomId 프롭이 시간이 지남에 따라 변경될 수 있습니다
function ChatRoom({ roomId }) { 
  useEffect(() => {
    // 이 효과가 roomId를 읽습니다
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    return () => {
      connection.disconnect();
    };
  // 따라서 이 효과가 roomId에 "의존한다"고 리액트에게 알려줍니다
  }, [roomId]);
  // ...

작동 방식은 다음과 같습니다.

  1. roomId가 프롭이고, 시간이 지남에 따라 변경될 수 있다는 사실을 알고 있습니다.
  2. 효과가 roomId를 읽는다는 것을 알고 있습니다(따라서 로직은 나중에 변경될 수 있는 값을 의존합니다).
  3. 따라서 이를 효과의 의존성으로 지정하여 (roomId가 변경될 때 효과가 다시 동기화되도록) 리액트에게 알려줍니다.

컴포넌트가 다시 렌더링된 후마다 리액트는 전달한 의존성 배열을 확인합니다. 배열 내의 값 중 어떤 값이 이전 렌더링 때 전달한 값과 다른 경우, 리액트는 효과(Efffect)를 다시 동기화합니다.

예를 들어, 초기 렌더링 시에 ["general"]을 전달하고, 다음 렌더링에서 ["travel"]을 전달한다면, 리액트는 "general"과 "travel"을 비교합니다. 이 값들은 다른 값입니다(Object.is를 사용하여 비교), 따라서 리액트는 효과를 다시 동기화합니다. 반대로, 컴포넌트가 다시 렌더링되었지만 roomId가 변경되지 않았다면, 효과는 여전히 같은 방에 연결된 상태를 유지할 것입니다.

각 효과(Efffect)는 별개의 동기화 과정을 나타냅니다

이미 작성한 효과에 이미 작성한 효과와 동시에 실행되어야 하는 관련 없는 로직을 추가하지 마십시오. 예를 들어, 사용자가 방을 방문할 때 분석 이벤트를 보내려고 합니다. 이미 roomId에 의존하는 효과가 있으므로 이곳에 분석 호출을 추가하고 싶을 수 있습니다.

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

그러나 나중에 연결을 다시 설정해야 하는 추가 종속성을 이 효과에 추가하게 되면, 이 효과가 다시 동기화될 때 같은 방에 대해 logVisit(roomId)가 호출되기 때문에 의도하지 않은 결과가 발생할 수 있습니다. 방문 기록 남기기는 연결과는 별개의 과정입니다. 이 두 개의 효과로 나누는 것이 좋습니다.

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

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

코드에서 각 효과(Efffect)는 별개이고 독립된 동기화 과정을 나타내어야 합니다.

위 예시에서 하나의 효과를 삭제해도 다른 효과의 로직이 깨지지 않습니다. 이는 서로 다른 동기화 대상을 동기화하고 있기 때문에 나누는 것이 올바른 지표입니다. 그러나 하나의 응집된 로직을 서로 다른 효과로 분리하면 코드가 "더 깔끔"해보일 수 있지만 유지보수가 어려워질 수 있습니다. 이러한 이유로 프로세스가 동일한지 또는 별개인지를 생각해야 하며, 코드가 더 깔끔해 보이는지는 고려하지 않아야 합니다.

효과(Effect)는 반응형 값에 "반응"합니다

당신의 효과는 두 개의 변수(serverUrlroomId)를 읽지만, 의존성으로서 roomId만 지정했습니다.

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

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

serverUrl을 의존성으로 지정하지 않아도 되는 걸까요?

그 이유는 serverUrl은 다시 렌더링으로 인해 변경되지 않기 때문입니다. 컴포넌트가 몇 번이고 다시 렌더링되는 동안 serverUrl은 항상 동일합니다. serverUrl은 변경되지 않으므로 의존성으로 지정하는 것은 의미가 없습니다. 결국, 의존성은 시간이 지남에 따라 값이 변경될 때에만 동작하는 것입니다!

반면에 roomId는 다시 렌더링할 때마다 다를 수 있습니다. 프롭, 상태 및 컴포넌트 내에서 선언된 기타 값은 렌더링 중에 계산되고 리액트 데이터 플로우에 참여하기 때문에 반응형입니다.

만약 serverUrl이 상태 변수였다면, 반응형이 될 것입니다. 반응형 값은 의존성에 포함되어야 합니다.

// 프롭은 시간이 지나면 변경될 수 있습니다
function ChatRoom({ roomId }) { 
  // 상태는 시간이 지나면 변경될 수 있습니다
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); 

  useEffect(() => {
    // 효과가 프롭과 상태를 읽습니다
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    return () => {
      connection.disconnect();
    };
  // 따라서 이 효과가 프롭과 상태에 "의존한다"고 리액트에게 알려줍니다
  }, [roomId, serverUrl]); 
  // ...
}

serverUrl을 의존성으로 포함함으로써, 값이 변경될 때 효과가 다시 동기화되도록 보장합니다.

이 샌드박스에서 선택한 채팅방을 변경하거나 서버 URL을 편집해 보세요:

Edit sleepy-tree-uimc78

roomIdserverUrl과 같은 반응형 값이 변경될 때마다 효과가 채팅 서버에 다시 연결하는 것을 확인할 수 있습니다.

의존성이 없는 효과의 의미

serverUrlroomId을 컴포넌트 외부로 옮겼을 때 어떻게 될까요?

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

function ChatRoom() {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []); // ✅ 모든 의존성 선언됨
  // ...
}

이제 효과의 코드는 반응형 값이 전혀 사용되지 않으므로, 의존성은 비어 있는 ([]) 상태일 수 있습니다.

컴포넌트의 관점에서 생각해보면, 비어 있는 [] 의존성 배열은 이 효과가 컴포넌트가 마운트될 때만 채팅방에 연결되고, 컴포넌트가 언마운트될 때만 연결을 해제한다는 의미입니다. (리액트는 여전히 개발 중에 한 번 더 동기화하기 위해 효과를 테스트합니다.)

Edit adoring-monad-ch0uj1

그러나 효과의 관점에서 생각해보면, 마운트와 언마운트에 대해 생각할 필요가 없습니다. 중요한 것은 효과가 동기화를 시작하고 중지하는 방법을 명시했다는 것입니다. 현재로서는 반응형 종속성이 없습니다. 그러나 사용자가 나중에 roomIdserverUrl을 시간이 지남에 따라 변경하길 원한다면 (그리고 이 값들이 반응형이 된다면), 효과의 코드는 변경되지 않을 것입니다. 단지 의존성을 추가하기만 하면 됩니다.

컴포넌트 내에서 선언된 모든 변수는 반응형입니다

프롭과 상태만이 반응형 값은 아닙니다. 이들에서 계산한 값들도 반응형입니다. 만약 프롭이나 상태가 변경되면 컴포넌트는 다시 렌더링되고, 이들로부터 계산된 값들도 함께 변경됩니다. 이러한 이유로 효과에서 사용되는 컴포넌트 본문의 모든 변수는 효과의 의존성 목록에 포함되어야 합니다.

예를 들어, 사용자는 드롭다운에서 채팅 서버를 선택할 수 있지만, 설정에서 기본 서버를 구성할 수도 있습니다. 이미 설정 상태를 컨텍스트에 넣어두었으므로 해당 컨텍스트에서 설정을 읽습니다. 이제 선택한 서버와 기본 서버를 기반으로 serverUrl을 계산합니다.

// roomId는 반응형입니다
function ChatRoom({ roomId, selectedServerUrl }) { 
  // settings은 반응형입니다
  const settings = useContext(SettingsContext); 
  // serverUrl은 반응형입니다
  const serverUrl = selectedServerUrl ?? settings.defaultServerUrl; 
  useEffect(() => {
    // 효과는 roomId와 serverUrl을 읽습니다
    const connection = createConnection(serverUrl, roomId); 
    connection.connect();
    return () => {
      connection.disconnect();
    };
  // 따라서 둘 중 하나라도 변경될 때 다시 동기화되어야 합니다!
  }, [roomId, serverUrl]); 
  // ...
}

이 예제에서 serverUrl은 프롭이나 상태 변수가 아닙니다. 렌더링 중에 계산되는 일반 변수입니다. 그러나 렌더링 중에 계산되므로 다시 렌더링으로 인해 변경될 수 있습니다. 이것이 반응형인 이유입니다.

컴포넌트 내부의 모든 값(프롭, 상태, 컴포넌트 본문의 변수 포함)은 반응형입니다. 반응형 값은 다시 렌더링에 따라 변경될 수 있으므로 효과의 의존성으로 포함해야 합니다.

다시 말해서, 효과는 컴포넌트 내부의 모든 값에 "반응"합니다.

자세히 알아보기: 전역 변수나 가변 값은 의존성이 될 수 있을까요?

가변 값(전역 변수를 포함한)은 반응형이 아닙니다.

location.pathname과 같은 가변 값은 의존성이 될 수 없습니다. 이 값은 가변이므로 리액트 렌더링 데이터 플로우 외부에서 언제든지 변경될 수 있습니다. 이를 변경해도 컴포넌트가 다시 렌더링되지 않습니다. 따라서 의존성에 지정해도 리액트는 값이 변경될 때 효과를 다시 동기화해야 한다는 사실을 알지 못합니다. 이는 또한 리액트의 규칙을 위반하는 것입니다. 렌더링 중(의존성을 계산하는 시점)에 가변 데이터를 읽는 것은 렌더링의 순수성을 깨뜨립니다. 대신 useSyncExternalStore를 사용하여 외부 가변 값에 대해 읽고 구독해야 합니다.

ref.current와 같은 가변 값이나 그 값에서 읽은 것들도 의존성이 될 수 없습니다. useRef에 의해 반환된 ref 객체 자체는 의존성이 될 수 있지만, 그 current 속성은 일부로 가변적으로 유지됩니다. 이를 통해 다시 렌더링을 트리거하지 않고 무언가를 추적할 수 있습니다. 그러나 변경해도 다시 렌더링을 트리거하지 않으므로 반응형 값이 아니며, 리액트는 값이 변경될 때 효과를 다시 실행할 지 알지 못합니다.

이 페이지에서 아래에서 알아보겠지만, 린터가 이러한 문제를 자동으로 확인합니다.

리액트는 모든 반응형 값이 의존성으로 선언되었는지 확인합니다

만약 린터가 리액트로 구성된 경우, Effect의 코드에서 사용하는 모든 반응형 값이 해당 의존성으로 선언되어 있는지 확인합니다. 예를 들어, 다음은 린터 오류입니다. 왜냐하면 roomIdserverUrl 모두 반응형이기 때문입니다.

Edit eager-field-f7vpbl

이는 리액트의 오류처럼 보일 수 있지만, 실제로는 리액트가 코드에서 버그를 가리키고 있습니다. roomIdserverUrl은 시간이 지남에 따라 변경될 수 있지만, 변경 사항이 발생할 때 Effect를 다시 동기화하지 않고 있습니다. UI에서 사용자가 다른 값을 선택해도 초기의 roomIdserverUrl에 연결된 상태로 남게 됩니다.

이 버그를 수정하려면 린터의 제안에 따라 Effect의 의존성으로 roomIdserverUrl을 지정하세요.

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

위의 예시를 상단의 샌드박스에서 시도해 보세요. 린터 오류가 해결되고 필요할 때마다 채팅이 다시 연결되는지 확인하세요.

참고
일부 경우에는 리액트가 컴포넌트 내부에 선언되었지만 절대로 변경되지 않는 값을 알고 있습니다. 예를 들어, useState에서 반환된 set 함수와 useRef에서 반환된 ref 객체는 안정적인(stable) 값입니다. 즉, 다시 렌더링될 때 변경되지 않는 것이 보장됩니다. 안정적인 값은 반응형이 아니므로 의존성 목록에서 생략할 수 있습니다. 포함시키는 것도 허용됩니다. 값이 변경되지 않기 때문에 중요하지 않습니다.

다시 동기화를 원하지 않을 때는 어떻게 해야 할까요?

이전 예시에서는 roomIdserverUrl을 의존성으로 나열하여 린터 오류를 수정했습니다.

그러나 대신 린터에게 이러한 값이 반응형 값이 아니라고 "증명"할 수 있습니다. 즉, 이러한 값들이 다시 렌더링으로 인해 변경되지 않을 수 있다는 것을 의미합니다. 예를 들어, 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
  // ...
}

또는 이들을 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
  // ...
}

Effect는 반응형 코드 블록입니다. 내부에서 읽은 값이 변경되면 다시 동기화됩니다. 이벤트 핸들러와는 달리, Effect는 필요한 동기화가 발생할 때마다 실행됩니다.

의존성을 "선택"할 수는 없습니다. 의존성은 반응형 값을 포함해야 합니다. 린터는 이를 강제합니다. 때로는 이로 인해 무한 루프와 너무 자주 동기화되는 문제가 발생할 수 있습니다. 린터를 억제해서 이러한 문제를 해결하지 마세요! 대신 다음을 시도해 보세요:

  • Effect가 동기화하는 독립적인 동기화 프로세스를 나타내는지 확인하세요. Effect가 아무것도 동기화하지 않는다면, 불필요한 것일 수 있습니다. Effect가 여러 개의 독립적인 것을 동기화한다면, 분리해야 합니다.
  • Effect에서 반응하지 않고 최신 props 또는 state 값을 읽고 싶을 때, Effect를 반응적인 부분(Effect 내에 유지하는 부분)과 반응하지 않는 부분(이벤트로 이루어진 Effect Event라고 하는 것으로 추출)으로 분리할 수 있습니다. Event와 Effect를 분리하는 방법에 대해 읽어보세요.
  • 객체와 함수를 의존성으로 사용하는 것을 피하세요. 객체와 함수를 렌더링 중에 생성한 다음 Effect에서 읽는 경우, 이들은 모든 렌더링마다 다른 값을 가지게 됩니다. 이로 인해 Effect가 매번 다시 동기화되므로 주의가 필요합니다. Effect에서 불필요한 의존성을 제거하는 방법에 대해 더 읽어보세요.

주의하기

린터는 당신의 친구입니다. 하지만 린터의 능력은 제한적입니다. 린터는 의존성이 잘못된 경우에만 알고 있습니다. 각 경우에 가장 적합한 해결 방법을 알지 못합니다. 린터가 의존성을 제안하지만 해당 의존성을 추가하면 루프가 발생하는 경우, 그것은 린터를 무시해야 한다는 의미는 아닙니다. 대신 의존성이 반응형이 아니고 의존성이 될 필요가 없도록 코드 내부(또는 외부)를 변경해야 합니다.

기존의 코드베이스가 있는 경우, 다음과 같이 린터를 억제하는 몇 가지 Effect가 있을 수 있습니다.

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

다음 페이지에서, 이 코드를 규칙을 위반하지 않고 수정하는 방법을 배우게 될 것입니다. 항상 수정하는 것이 가치가 있습니다!

요약

  • 컴포넌트는 마운트, 업데이트, 언마운트될 수 있습니다.
  • 각 Effect는 주변 컴포넌트와는 별개의 라이프사이클을 가집니다.
  • 각 Effect는 별도의 동기화 프로세스를 설명하며 시작하고 중지할 수 있습니다.
  • Effect를 작성하고 읽을 때는 컴포넌트의 관점에서 생각하기보다 각 개별 Effect의 관점에서 생각하는 것이 중요합니다(동기화를 시작하고 중지하는 방법).
  • 컴포넌트 내부에 선언된 값들은 "반응형"입니다.
  • 반응형 값은 시간이 지남에 따라 변경될 수 있기 때문에 Effect를 다시 동기화해야 합니다.
  • 린터는 Effect 내부에서 사용되는 모든 반응형 값을의존성으로 지정하는지 확인합니다.
  • 린터에 의해 표시된 모든 오류는 정당합니다. 규칙을 어길 수 있는 방법은 항상 있습니다.