v19.2Latest

반응형 Effect의 생명주기

Effect는 컴포넌트와 다른 생명주기를 가집니다. 컴포넌트는 마운트, 업데이트, 언마운트될 수 있습니다. Effect는 두 가지 작업만 수행할 수 있습니다: 무언가를 동기화하기 시작하고, 나중에 동기화를 중지하는 것입니다. 시간이 지남에 따라 변경되는 props와 state에 Effect가 의존하는 경우 이 주기는 여러 번 발생할 수 있습니다. React는 Effect의 의존성을 올바르게 지정했는지 확인하기 위한 린터 규칙을 제공합니다. 이를 통해 Effect가 최신 props 및 state와 동기화된 상태를 유지합니다.

배울 내용
  • Effect의 생명주기가 컴포넌트의 생명주기와 어떻게 다른지
  • 각각의 Effect를 개별적으로 어떻게 생각해야 하는지
  • Effect가 언제 다시 동기화되어야 하는지, 그리고 그 이유
  • Effect의 의존성이 어떻게 결정되는지
  • 값이 반응형이라는 것이 무엇을 의미하는지
  • 빈 의존성 배열이 의미하는 것
  • React가 린터를 통해 의존성이 올바른지 어떻게 확인하는지
  • 린터와 의견이 다를 때 해야 할 일

Effect의 생명주기

모든 React 컴포넌트는 동일한 생명주기를 거칩니다:

  • 컴포넌트는 화면에 추가될 때마운트됩니다.
  • 컴포넌트는 새로운 props나 state를 받을 때, 일반적으로 상호작용에 대한 응답으로업데이트됩니다.
  • 컴포넌트는 화면에서 제거될 때언마운트됩니다.

컴포넌트에 대해 생각하는 좋은 방법이지만, Effect에 대해서는그렇지 않습니다.대신, 각 Effect를 컴포넌트의 생명주기와 독립적으로 생각해 보세요. Effect는 외부 시스템을 현재 props 및 state와동기화하는 방법을 설명합니다. 코드가 변경됨에 따라 동기화는 더 자주 또는 덜 자주 발생해야 할 수 있습니다.

이 점을 설명하기 위해, 컴포넌트를 채팅 서버에 연결하는 다음 Effect를 고려해 보세요:

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

Effect가 반환하는 정리 함수는동기화를 중지하는 방법을 지정합니다:

직관적으로, React가 컴포넌트가 마운트될 때동기화를 시작하고 컴포넌트가 언마운트될 때동기화를 중지할 것이라고 생각할 수 있습니다. 그러나 이것이 끝이 아닙니다! 때로는 컴포넌트가 마운트된 상태로 유지되는 동안동기화를 여러 번 시작하고 중지해야 할 수도 있습니다.

이것이 필요한지, 언제발생하는지, 그리고 이 동작을어떻게제어할 수 있는지 살펴보겠습니다.

참고

일부 Effect는 정리 함수를 전혀 반환하지 않습니다.대부분의 경우, 정리 함수를 반환하고 싶겠지만, 그렇지 않다면 React는 빈 정리 함수를 반환한 것처럼 동작합니다.

동기화가 여러 번 발생해야 할 수 있는 이유

사용자가 드롭다운에서 선택하는roomIdChatRoomprop을 받는 이 컴포넌트를 상상해 보세요. 처음에 사용자가 roomId"general"방을로 선택한다고 가정해 보겠습니다. 앱은"general"채팅 방을 표시합니다:

UI가 표시된 후, React는 Effect를 실행하여동기화를 시작합니다. 이는 "general" 방에 연결됩니다:

지금까지는 괜찮습니다.

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

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

이 시점에서 React가 두 가지 작업을 수행하기를 원합니다:

  1. 이전 roomId와의 동기화를 중지합니다("general"방에서 연결 해제).
  2. 새로운 roomId와의 동기화를 시작합니다("travel"방에 연결).

다행히도, 이미 React에게 이 두 가지를 수행하는 방법을 가르쳤습니다!Effect의 본문은 동기화를 시작하는 방법을 지정하고, 클린업 함수는 동기화를 중지하는 방법을 지정합니다. 이제 React가 해야 할 일은 올바른 순서와 올바른 props 및 state로 이들을 호출하는 것뿐입니다. 정확히 어떻게 일어나는지 살펴보겠습니다.

React가 Effect를 다시 동기화하는 방법

여러분의 ChatRoom 컴포넌트가 roomIdprop에 대한 새로운 값을 받았다는 점을 기억하세요. 이전에는"general"이었고, 지금은"travel"입니다. React는 다른 방에 다시 연결하기 위해 Effect를 다시 동기화해야 합니다.

동기화를중지하기 위해, React는 "general"방에 연결한 후 Effect가 반환한 클린업 함수를 호출합니다.roomId"general"이었기 때문에, 클린업 함수는"general"방에서 연결을 해제합니다:

그런 다음 React는 이번 렌더링 중에 제공한 Effect를 실행합니다. 이번에는roomId"travel"이므로 동기화를 시작하여 "travel"채팅방에 연결합니다(결국 클린업 함수도 호출될 때까지):

이 덕분에 이제 사용자가 UI에서 선택한 방과 동일한 방에 연결되었습니다. 재앙을 피했습니다!

컴포넌트가 다른 roomId로 다시 렌더링될 때마다 Effect는 다시 동기화됩니다. 예를 들어, 사용자가roomId"travel"에서"music"으로 변경한다고 가정해 보겠습니다중지합니다. 그런 다음 새로운 "travel"방에서 연결 해제) Effect의 동기화를동기화를 시작합니다.roomIdprop으로 본문을 실행하여("music"방에 연결) 다시

마지막으로, 사용자가 다른 화면으로 이동하면ChatRoom이 언마운트됩니다. 이제 더 이상 연결을 유지할 필요가 전혀 없습니다. React는 Effect의 동기화를마지막으로 중지하고 "music"채팅방에서 연결을 해제합니다.

Effect의 관점에서 생각하기

지금까지 일어난 모든 일을 ChatRoom컴포넌트의 관점에서 요약해 보겠습니다:

  1. ChatRoomroomId"general"로 설정된 상태로 마운트됨
  2. ChatRoomroomId"travel"로 설정된 상태로 업데이트됨
  3. ChatRoomroomId"music"로 설정된 상태로 업데이트됨
  4. ChatRoom이 언마운트됨

컴포넌트 생명주기의 각 지점에서 여러분의 Effect는 다른 작업을 수행했습니다:

  1. 여러분의 Effect가"general"채팅방에 연결됨
  2. 여러분의 Effect가"general"채팅방에서 연결 해제되고"travel"채팅방에 연결됨
  3. 여러분의 Effect가"travel"채팅방에서 연결 해제되고"music"채팅방에 연결됨
  4. 여러분의 Effect가"music"채팅방에서 연결 해제됨

이제 Effect 자체의 관점에서 무슨 일이 일어났는지 생각해 봅시다:

이 코드의 구조는 여러분에게 겹치지 않는 시간대의 연속으로 일어난 일을 보도록 영감을 줄 수 있습니다:

  1. 여러분의 Effect가"general"채팅방에 연결됨 (연결 해제될 때까지)
  2. 여러분의 Effect가"travel"채팅방에 연결됨 (연결 해제될 때까지)
  3. 여러분의 Effect가"music"채팅방에 연결됨 (연결 해제될 때까지)

이전에는 컴포넌트의 관점에서 생각하고 있었습니다. 컴포넌트의 관점에서 볼 때, Effect를 "렌더링 후"나 "언마운트 전"과 같은 특정 시점에 실행되는 "콜백"이나 "생명주기 이벤트"로 생각하기 쉽습니다. 이런 사고 방식은 매우 빠르게 복잡해지므로 피하는 것이 좋습니다.

대신, 항상 한 번에 하나의 시작/중지 주기에 집중하세요. 컴포넌트가 마운트 중인지, 업데이트 중인지, 언마운트 중인지는 중요하지 않아야 합니다. 여러분이 해야 할 일은 동기화를 시작하는 방법과 중지하는 방법을 설명하는 것뿐입니다. 이를 잘 수행하면 여러분의 Effect는 필요한 만큼 여러 번 시작되고 중지되더라도 견고하게 작동할 것입니다.

이는 JSX를 생성하는 렌더링 로직을 작성할 때 컴포넌트가 마운트 중인지 업데이트 중인지 생각하지 않는 방식과 비슷할 수 있습니다. 여러분은 화면에 무엇이 표시되어야 하는지 설명하면, React가나머지를 알아냅니다.

React가 여러분의 Effect가 재동기화될 수 있는지 확인하는 방법

다음은 직접 실험해 볼 수 있는 라이브 예제입니다. "채팅 열기"를 눌러ChatRoom컴포넌트를 마운트하세요:

컴포넌트가 처음 마운트될 때 세 개의 로그가 표시되는 것을 확인하세요:

  1. ✅ Connecting to "general" room at https://localhost:1234...(개발 환경 전용)
  2. ❌ Disconnected from "general" room at https://localhost:1234.(개발 환경 전용)
  3. ✅ Connecting to "general" room at https://localhost:1234...

처음 두 로그는 개발 환경 전용입니다. 개발 환경에서는 React가 항상 각 컴포넌트를 한 번씩 다시 마운트합니다.

React는 개발 환경에서 즉시 Effect를 다시 동기화하도록 강제하여 Effect가 다시 동기화될 수 있는지 확인합니다.이는 문을 열고 한 번 더 닫아서 문 잠금 장치가 작동하는지 확인하는 것을 떠올리게 할 수 있습니다. React는 개발 환경에서정리(cleanup)를 잘 구현했는지 확인하기 위해 Effect를 한 번 더 시작하고 중지합니다.

실제로 Effect가 다시 동기화되는 주요 이유는 Effect가 사용하는 일부 데이터가 변경되었기 때문입니다. 위 샌드박스에서 선택된 채팅방을 변경해 보세요.roomId가 변경될 때 Effect가 다시 동기화되는 방식을 확인하세요.

그러나 다시 동기화가 필요한 더 특이한 경우도 있습니다. 예를 들어, 채팅이 열려 있는 동안 위 샌드박스에서serverUrl을 편집해 보세요. 코드를 편집하는 것에 대한 응답으로 Effect가 다시 동기화되는 방식을 확인하세요. 향후 React는 다시 동기화에 의존하는 더 많은 기능을 추가할 수 있습니다.

React가 Effect를 다시 동기화해야 한다는 것을 어떻게 아는가

React가 roomId변경 후 Effect를 다시 동기화해야 한다는 것을 어떻게 알았는지 궁금할 수 있습니다. 그것은여러분이 React에게 해당 코드가 roomId에 의존한다고 의존성 배열에 포함시켜 알려주었기 때문입니다:

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

  1. 여러분은roomId가 prop이며, 이는 시간이 지남에 따라 변경될 수 있다는 것을 알고 있었습니다.
  2. 여러분은 Effect가roomId를 읽는다는 것을 알고 있었습니다(따라서 그 논리는 나중에 변경될 수 있는 값에 의존합니다).
  3. 이것이 여러분이 이를 Effect의 의존성으로 지정한 이유입니다(따라서roomId가 변경될 때 다시 동기화됩니다).

컴포넌트가 다시 렌더링될 때마다 React는 여러분이 전달한 의존성 배열을 살펴볼 것입니다. 배열의 어떤 값이 이전 렌더링 동안 전달한 동일한 위치의 값과 다르다면 React는 Effect를 다시 동기화할 것입니다.

예를 들어, 초기 렌더링 중에["general"]을 전달했고 나중에 다음 렌더링 중에["travel"]을 전달했다면, React는"general""travel"을 비교할 것입니다. 이 값들은 (Object.is로 비교했을 때) 다른 값이므로 React는 Effect를 다시 동기화할 것입니다. 반면, 컴포넌트가 다시 렌더링되었지만roomId가 변경되지 않았다면 Effect는 동일한 방에 계속 연결된 상태로 유지될 것입니다.

각 Effect는 별도의 동기화 프로세스를 나타냅니다

이미 작성한 Effect와 동시에 실행되어야 한다는 이유만으로 관련 없는 로직을 Effect에 추가하려는 유혹을 피하세요. 예를 들어, 사용자가 방을 방문할 때 분석 이벤트를 보내고 싶다고 가정해 보겠습니다. 이미roomId에 의존하는 Effect가 있으므로 분석 호출을 거기에 추가하고 싶은 유혹을 느낄 수 있습니다:

하지만 나중에 이 Effect에 연결을 다시 설정해야 하는 다른 의존성을 추가한다고 상상해 보세요. 이 Effect가 다시 동기화되면 의도하지 않은 동일한 방에 대해logVisit(roomId)도 호출할 것입니다. 방문 기록 로깅은 연결과별도의 프로세스입니다. 두 개의 별도 Effect로 작성하세요:

코드의 각 Effect는 별도이고 독립적인 동기화 프로세스를 나타내야 합니다.

위 예제에서 하나의 Effect를 삭제해도 다른 Effect의 논리가 깨지지 않습니다. 이것은 그들이 다른 것들을 동기화한다는 좋은 신호이며, 따라서 분리하는 것이 합리적이었습니다. 반면, 응집력 있는 논리 조각을 별도의 Effect로 분리하면 코드가 "더 깨끗해" 보일 수 있지만유지 관리하기 더 어려워질 수 있습니다.이것이 여러분이 코드가 더 깨끗해 보이는지가 아니라 프로세스가 동일한지 별개인지 생각해야 하는 이유입니다.

Effect는 반응형 값에 "반응"합니다

여러분의 Effect는 두 변수(serverUrlroomId)를 읽지만, 의존성으로roomId만 지정했습니다:

serverUrl이 의존성으로 필요하지 않을까요?

이는 serverUrl이 리렌더링으로 인해 절대 변경되지 않기 때문입니다. 컴포넌트가 몇 번이나, 왜 리렌더링되든 항상 동일합니다.serverUrl이 절대 변하지 않으므로, 의존성으로 지정하는 것은 의미가 없습니다. 결국 의존성은 시간이 지나면서 변경될 때만 의미가 있기 때문입니다!

반면에, roomId는 리렌더링 시 다를 수 있습니다.컴포넌트 내부에서 선언된 props, state 및 기타 값들은반응형입니다. 왜냐하면 이들은 렌더링 중에 계산되며 React 데이터 흐름에 참여하기 때문입니다.

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

의존성에serverUrl을 포함함으로써, 변경 후 Effect가 다시 동기화되도록 보장합니다.

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

반응형 값인 roomIdserverUrl을 변경할 때마다, Effect는 채팅 서버에 다시 연결됩니다.

의존성이 빈 Effect의 의미

만약 serverUrlroomId를 모두 컴포넌트 외부로 옮기면 어떻게 될까요?

이제 Effect의 코드는어떤반응형 값도 사용하지 않으므로, 의존성이 비어 있을 수 있습니다([]).

컴포넌트의 관점에서 생각해보면, 빈[]의존성 배열은 이 Effect가 컴포넌트가 마운트될 때만 채팅방에 연결하고, 컴포넌트가 언마운트될 때만 연결을 끊는다는 것을 의미합니다. (React는 여전히 개발 모드에서 로직을 스트레스 테스트하기 위해한 번 더 재동기화한다는 점을 기억하세요.)

하지만, Effect의 관점에서 생각한다면,마운트와 언마운트에 대해 전혀 생각할 필요가 없습니다. 중요한 것은 Effect가 동기화를 시작하고 중지하기 위해 무엇을 하는지 지정한 것입니다. 현재는 반응형 의존성이 없습니다. 하지만 사용자가 시간이 지남에 따라roomIdserverUrl을 변경하게 하고 싶다면(그러면 이들은 반응형이 될 것입니다), Effect의 코드는 변경되지 않을 것입니다. 단지 의존성에 추가하기만 하면 됩니다.

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

Props와 state만 반응형 값이 아닙니다. 이들로부터 계산된 값들도 반응형입니다. props나 state가 변경되면 컴포넌트가 리렌더링되고, 이들로부터 계산된 값들도 변경됩니다. 이것이 컴포넌트 본문에서 Effect가 사용하는 모든 변수가 Effect 의존성 목록에 있어야 하는 이유입니다.

사용자가 드롭다운에서 채팅 서버를 선택할 수 있지만, 설정에서 기본 서버를 구성할 수도 있다고 가정해 보겠습니다. 설정 state를 이미컨텍스트에 넣었다고 가정하고, 해당 컨텍스트에서settings를 읽습니다. 이제 props에서 선택된 서버와 기본 서버를 기반으로serverUrl을 계산합니다:

이 예시에서serverUrl은 prop이나 state 변수가 아닙니다. 렌더링 중에 계산되는 일반 변수입니다. 하지만 렌더링 중에 계산되므로 리렌더링으로 인해 변경될 수 있습니다. 이것이 반응형인 이유입니다.

컴포넌트 내부의 모든 값(props, state, 컴포넌트 본문의 변수 포함)은 반응형입니다. 모든 반응형 값은 리렌더링 시 변경될 수 있으므로, 반응형 값을 Effect의 의존성으로 포함해야 합니다.

다시 말해, Effect는 컴포넌트 본문의 모든 값에 "반응"합니다.

Deep Dive
전역 또는 가변 값이 의존성이 될 수 있나요?

React는 모든 반응형 값이 의존성으로 지정되었는지 확인합니다

린터가 React용으로 구성된 경우,Effect 코드에서 사용하는 모든 반응형 값이 의존성으로 선언되었는지 확인합니다. 예를 들어, 다음은roomIdserverUrl이 모두 반응형이므로 린트 오류입니다:

이는 React 오류처럼 보일 수 있지만, 실제로 React는 코드의 버그를 지적하고 있습니다.roomIdserverUrl은 시간이 지남에 따라 변경될 수 있지만, 변경 시 Effect를 재동기화하는 것을 잊고 있습니다. 사용자가 UI에서 다른 값을 선택한 후에도 초기roomIdserverUrl에 계속 연결된 상태로 남게 됩니다.

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

위 샌드박스에서 이 수정을 시도해 보세요. 린터 오류가 사라지고 필요할 때 채팅이 다시 연결되는지 확인하세요.

참고

경우에 따라 React는 컴포넌트 내부에 선언되었더라도 값이 절대 변경되지 않는다는 것을알고 있습니다. 예를 들어, set 함수가 반환하는useStateuseRef가 반환하는 ref 객체는안정적입니다—리렌더링 시 변경되지 않음이 보장됩니다. 안정적인 값은 반응형이 아니므로 목록에서 생략할 수 있습니다. 포함하는 것은 허용됩니다: 변경되지 않으므로 문제가 되지 않습니다.

재동기화를 원하지 않을 때 해야 할 일

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

하지만, 대신 이러한 값이 반응형 값이 아니라는 것을 린터에게 "증명"할 수도 있습니다,즉, 리렌더링 결과로변경될 수 없다는 것을 말입니다. 예를 들어, serverUrlroomId가 렌더링에 의존하지 않고 항상 동일한 값을 갖는다면, 컴포넌트 외부로 이동할 수 있습니다. 이제는 의존성이 될 필요가 없습니다:

또한 이들을 Effect 내부로 이동시킬 수도 있습니다.이 값들은 렌더링 중에 계산되지 않으므로 반응형이 아닙니다:

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

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

주의사항

린터는 당신의 친구이지만, 그 힘은 제한적입니다. 린터는 의존성이잘못되었을 때만알 수 있습니다. 각 경우를 해결하는최선의 방법은 알지 못합니다. 린터가 의존성을 제안하지만, 추가하면 루프가 발생한다고 해서 린터를 무시해서는 안 됩니다. 해당 값이 반응형이 아니고 의존성이 될필요가 없도록Effect 내부(또는 외부)의 코드를 변경해야 합니다.

기존 코드베이스가 있다면, 다음과 같이 린터를 억제하는 Effect가 있을 수 있습니다:

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

요약

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

Try out some challenges

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.

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.