v19.2Latest

Effect 의존성 제거하기

Effect를 작성할 때 린터는 Effect가 읽는 모든 반응형 값(예: props 및 state)을 Effect의 의존성 목록에 포함했는지 확인합니다. 이렇게 하면 Effect가 컴포넌트의 최신 props 및 state와 동기화된 상태를 유지할 수 있습니다. 불필요한 의존성은 Effect가 너무 자주 실행되거나 무한 루프를 생성할 수 있습니다. 이 가이드를 따라 Effect에서 불필요한 의존성을 검토하고 제거하는 방법을 알아보세요.

배울 내용
  • 무한 Effect 의존성 루프를 수정하는 방법
  • 의존성을 제거하고 싶을 때 해야 할 일
  • Effect에서 값을 "반응"하지 않고 읽는 방법
  • 객체 및 함수 의존성을 피하는 방법과 이유
  • 의존성 린터 억제가 위험한 이유와 대안

의존성은 코드와 일치해야 합니다

Effect를 작성할 때는 먼저 Effect가 수행하길 원하는 작업을시작하고 중지하는 방법을 지정합니다:

그런 다음 Effect 의존성을 비워두면([]), 린터가 올바른 의존성을 제안합니다:

린터가 지시하는 대로 의존성을 채웁니다:

Effect는 반응형 값에 "반응"합니다. roomId는 반응형 값(리렌더링으로 인해 변경될 수 있음)이므로, 린터는 이를 의존성으로 지정했는지 확인합니다.roomId가 다른 값을 받으면 React는 Effect를 다시 동기화합니다. 이렇게 하면 채팅이 선택한 방에 연결된 상태를 유지하고 드롭다운에 "반응"할 수 있습니다:

의존성을 제거하려면, 그것이 의존성이 아니라는 것을 증명하세요

Effect의 의존성을 "선택"할 수 없다는 점에 주목하세요. Effect 코드에서 사용되는 모든반응형 값은 의존성 목록에 선언되어야 합니다. 의존성 목록은 주변 코드에 의해 결정됩니다:

반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수와 함수가 포함됩니다.roomId는 반응형 값이므로 의존성 목록에서 제거할 수 없습니다. 린터가 이를 허용하지 않을 것입니다:

그리고 린터가 옳을 것입니다!roomId는 시간이 지남에 따라 변경될 수 있으므로, 이는 코드에 버그를 초래할 것입니다.

의존성을 제거하려면, 린터에게 그것이필요하지 않다는것을 "증명"하세요. 예를 들어, roomId를 컴포넌트 밖으로 이동시켜 그것이 반응형이 아니며 다시 렌더링될 때 변경되지 않음을 증명할 수 있습니다:

이제 roomId는 반응형 값이 아니므로(그리고 다시 렌더링될 때 변경될 수 없으므로) 의존성이 될 필요가 없습니다:

이것이 바로 이제 빈([]) 의존성 배열을 지정할 수 있는 이유입니다. 여러분의 Effect는더 이상반응형 값에 의존하지 않으므로, 컴포넌트의 props나 state가 변경될 때정말로 다시 실행될 필요가 없습니다.

의존성을 변경하려면 코드를 변경하세요

여러분의 작업 흐름에서 패턴을 발견했을 수도 있습니다:

  1. 먼저, Effect의 코드나 반응형 값이 선언된 방식을변경합니다.
  2. 그런 다음, 린터를 따라 변경한 코드에 맞게 의존성을조정합니다.
  3. 의존성 목록이 마음에 들지 않으면첫 번째 단계로 돌아가서(코드를 다시 변경합니다).

마지막 부분이 중요합니다.의존성을 변경하려면 먼저 주변 코드를 변경하세요. 의존성 배열은 Effect 코드에서 사용하는 모든 반응형 값의 목록으로 생각할 수 있습니다선택하지 않습니다. 그 목록은 여러분의 코드를설명합니다. 의존성 배열을 변경하려면 코드를 변경하세요.

이는 방정식을 푸는 것처럼 느껴질 수 있습니다. 목표(예: 의존성 제거)로 시작하여 그 목표에 맞는 코드를 "찾아야" 할 수도 있습니다. 모든 사람이 방정식 풀이를 즐기지는 않으며, Effect 작성도 마찬가지일 수 있습니다! 다행히도 아래에서 시도해 볼 수 있는 일반적인 레시피 목록이 있습니다.

주의사항

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

의존성이 코드와 일치하지 않으면 버그가 발생할 위험이 매우 높습니다.린터를 억제함으로써 여러분은 Effect가 의존하는 값에 대해 React에 "거짓말"을 하는 것입니다.

대신 아래 기술들을 사용하세요.

불필요한 의존성 제거하기

코드를 반영하도록 Effect의 의존성을 조정할 때마다 의존성 배열을 살펴보세요. 이러한 의존성 중 하나라도 변경될 때 Effect가 다시 실행되는 것이 합리적입니까? 때로는 그 대답이 "아니오"입니다:

  • 다른 조건에서 Effect의다른 부분을 다시 실행하고 싶을 수 있습니다.
  • 어떤 의존성의 변경에 "반응"하는 대신, 그 의존성의최신 값만 읽고 싶을 수 있습니다.
  • 의존성이 객체나 함수라서의도치 않게너무 자주 변경될 수 있습니다.

올바른 해결책을 찾으려면 Effect에 관한 몇 가지 질문에 답해야 합니다. 함께 살펴보겠습니다.

이 코드를 이벤트 핸들러로 옮겨야 할까요?

먼저 이 코드가 Effect여야 하는지 생각해야 합니다.

폼을 상상해 보세요. 제출 시submitted상태 변수를true로 설정합니다. POST 요청을 보내고 알림을 표시해야 합니다. 이 로직을submittedtrue가 되는 것에 "반응"하는 Effect 안에 넣었습니다:

나중에 현재 테마에 따라 알림 메시지 스타일을 지정하고 싶어서 현재 테마를 읽습니다.theme는 컴포넌트 본문에 선언되어 있으므로 반응형 값입니다. 따라서 이를 의존성으로 추가합니다:

이렇게 하면 버그가 발생합니다. 폼을 먼저 제출한 다음 다크 테마와 라이트 테마를 전환한다고 상상해 보세요.theme이 변경되고, Effect가 다시 실행되어 동일한 알림을 다시 표시할 것입니다!

여기서 문제는 이것이 처음부터 Effect가 아니어야 한다는 점입니다.이 POST 요청을 보내고 알림을 표시하려는 것은폼 제출이라는 특정 상호작용에 대한 응답입니다. 특정 상호작용에 대한 응답으로 코드를 실행하려면 해당 로직을 직접 해당 이벤트 핸들러에 넣으세요:

이제 코드가 이벤트 핸들러에 있으므로 반응형이 아닙니다. 따라서 사용자가 폼을 제출할 때만 실행됩니다.이벤트 핸들러와 Effect 중 선택하는 방법불필요한 Effect를 삭제하는 방법에 대해 더 읽어보세요.

Effect가 여러 개의 관련 없는 작업을 수행하고 있나요?

다음으로 스스로에게 물어봐야 할 질문은 Effect가 여러 개의 관련 없는 작업을 수행하고 있는지입니다.

사용자가 도시와 지역을 선택해야 하는 배송 폼을 만든다고 상상해 보세요. 선택된country에 따라 서버에서 cities목록을 가져와 드롭다운에 표시합니다:

이는 Effect에서 데이터를 가져오는 좋은 예시입니다. cities 상태를 countryprop에 따라 네트워크와 동기화하고 있습니다. 이벤트 핸들러에서는 이 작업을 할 수 없습니다. 왜냐하면ShippingForm이 표시되는 즉시 그리고country가 변경될 때마다(어떤 상호작용으로 인해 변경되든) 가져와야 하기 때문입니다.

이제 도시 지역을 위한 두 번째 선택 상자를 추가한다고 가정해 보세요. 이 상자는 현재 선택된city에 대한 areas를 가져와야 합니다. 동일한 Effect 내에 두 번째fetch호출을 추가하여 지역 목록을 가져올 수 있습니다:

하지만 이제 Effect가city상태 변수를 사용하기 때문에,city를 의존성 목록에 추가해야 했습니다. 이로 인해 문제가 발생했습니다: 사용자가 다른 도시를 선택하면 Effect가 다시 실행되어fetchCities(country)를 호출하게 됩니다. 결과적으로 도시 목록을 불필요하게 여러 번 다시 가져오게 됩니다.

이 코드의 문제점은 서로 관련 없는 두 가지를 동기화하고 있다는 것입니다:

  1. 네트워크를 기반으로cities 상태를 countryprop과 동기화하려 합니다.
  2. 네트워크를 기반으로areas 상태를 city상태와 동기화하려 합니다.

로직을 두 개의 Effect로 분리하여 각각 동기화해야 하는 prop에 반응하도록 합니다:

이제 첫 번째 Effect는country가 변경될 때만 다시 실행되고, 두 번째 Effect는city가 변경될 때 다시 실행됩니다. 목적에 따라 분리했습니다: 두 가지 다른 것이 두 개의 별도 Effect로 동기화됩니다. 두 개의 별도 Effect는 두 개의 별도 의존성 목록을 가지므로 서로 의도치 않게 트리거되지 않습니다.

최종 코드는 원본보다 길지만, 이러한 Effect를 분리하는 것은 여전히 올바릅니다.각 Effect는 독립적인 동기화 프로세스를 나타내야 합니다.이 예제에서 하나의 Effect를 삭제해도 다른 Effect의 로직이 깨지지 않습니다. 이는 서로다른 것을 동기화한다는 의미이며, 분리하는 것이 좋습니다. 중복이 걱정된다면 반복되는 로직을 커스텀 훅으로 추출하여 이 코드를 개선할 수 있습니다.

다음 상태를 계산하기 위해 일부 상태를 읽고 있나요?

이 Effect는 새 메시지가 도착할 때마다 새로 생성된 배열로messages 상태 변수를 업데이트합니다:

이는 messages변수를 사용하여새 배열을 생성하는데, 기존 메시지 전체로 시작하여 새 메시지를 끝에 추가합니다. 그러나messages는 Effect에서 읽는 반응형 값이므로 의존성이어야 합니다:

그리고 messages를 의존성으로 만드는 것은 문제를 야기합니다.

메시지를 받을 때마다,setMessages()는 수신된 메시지를 포함하는 새로운messages배열로 컴포넌트를 다시 렌더링하게 합니다. 그러나 이제 이 Effect가messages에 의존하기 때문에, 이는 Effect도또한다시 동기화하게 합니다. 따라서 모든 새 메시지는 채팅이 다시 연결되게 만듭니다. 사용자는 이를 좋아하지 않을 것입니다!

문제를 해결하려면 Effect 내부에서messages를 읽지 마세요. 대신,업데이터 함수setMessages에 전달하세요:

이제 Effect가 messages변수를 전혀 읽지 않는다는 점에 주목하세요. 이제는 msgs => [...msgs, receivedMessage]와 같은 업데이터 함수만 전달하면 됩니다업데이터 함수를 큐에 넣고다음 렌더링 시에msgs인수를 제공합니다. 이것이 바로 Effect 자체가 더 이상messages에 의존할 필요가 없는 이유입니다. 이 수정으로 인해 채팅 메시지를 수신해도 채팅이 다시 연결되지 않게 됩니다.

값을 읽되 그 변경에 "반응"하지 않으려면 어떻게 해야 할까요?

사용자가 새 메시지를 받을 때 isMutedtrue가 아닌 경우에만 소리를 재생하고 싶다고 가정해 보겠습니다:

이제 Effect가 코드에서isMuted를 사용하므로 의존성에 추가해야 합니다:

문제는 isMuted가 변경될 때마다(예: 사용자가 "음소거" 토글을 누를 때) Effect가 다시 동기화되어 채팅에 다시 연결된다는 것입니다. 이는 원하는 사용자 경험이 아닙니다! (이 예시에서는 린터를 비활성화해도 작동하지 않습니다. 그렇게 하면isMuted가 이전 값에 "고착"될 것입니다.)

이 문제를 해결하려면 반응적이지 않은 로직을 Effect에서 추출해야 합니다. 이 Effect가isMuted의 변경에 "반응"하지 않기를 원합니다.이 반응적이지 않은 로직 조각을 Effect Event로 이동하세요:

Effect Event를 사용하면 Effect를 반응적인 부분(roomId와 같은 반응형 값과 그 변경에 "반응"해야 하는 부분)과 비반응적인 부분(onMessageisMuted를 읽는 것처럼 최신 값만 읽는 부분)으로 분리할 수 있습니다.이제 Effect Event 내부에서isMuted를 읽었으므로, Effect의 의존성이 될 필요가 없습니다.결과적으로 "음소거" 설정을 켜고 끌 때 채팅이 다시 연결되지 않아 원래 문제가 해결됩니다!

props에서 이벤트 핸들러 감싸기

컴포넌트가 prop으로 이벤트 핸들러를 받을 때 비슷한 문제가 발생할 수 있습니다:

부모 컴포넌트가 매 렌더링마다다른onReceiveMessage함수를 전달한다고 가정해 보겠습니다:

의존성이므로onReceiveMessage는 부모가 다시 렌더링될 때마다 Effect가 다시 동기화되게 하여 채팅에 다시 연결하게 만듭니다. 이 문제를 해결하려면 호출을 Effect Event로 감싸세요:

Effect Event는 반응적이지 않으므로 의존성으로 지정할 필요가 없습니다. 결과적으로 부모 컴포넌트가 매번 다른 함수를 전달하더라도 채팅이 다시 연결되지 않습니다.

반응적 코드와 비반응적 코드 분리하기

이 예시에서는 roomId가 변경될 때마다 방문을 기록하고 싶습니다. 모든 로그에 현재notificationCount를 포함시키고 싶지만,그렇다고해서 notificationCount의 변경이 로그 이벤트를 트리거하게 하고 싶지는 않습니다.

해결책은 다시 한번 비반응적 코드를 Effect Event로 분리하는 것입니다:

여러분의 로직이 roomId에 대해 반응적으로 동작하기를 원하므로, Effect 내부에서roomId를 읽습니다. 그러나 notificationCount가 변경되어 추가 방문 기록이 로깅되는 것을 원하지 않으므로, Effect Event 내부에서notificationCount를 읽습니다. Effect Event를 사용하여 Effect에서 최신 props와 state를 읽는 방법에 대해 자세히 알아보세요.

어떤 반응형 값이 의도치 않게 변경되나요?

예를 들어, 컴포넌트 본문에서options 객체를 생성한 다음 Effect 내부에서 그 객체를 읽는 경우를 생각해 보세요:

이 객체는 컴포넌트 본문에서 선언되었으므로반응형 값입니다. Effect 내부에서 이와 같은 반응형 값을 읽을 때는 의존성으로 선언해야 합니다. 이렇게 하면 Effect가 그 변경 사항에 "반응"하도록 보장됩니다:

의존성으로 선언하는 것이 중요합니다! 예를 들어,roomId가 변경되면 Effect가 새로운 options로 채팅에 다시 연결되도록 보장합니다. 그러나 위 코드에는 문제도 있습니다. 아래 샌드박스의 입력란에 타이핑하여 콘솔에서 어떤 일이 발생하는지 확인해 보세요:

위 샌드박스에서 입력란은 message상태 변수만 업데이트합니다. 사용자 관점에서 이는 채팅 연결에 영향을 주지 않아야 합니다. 그러나message를 업데이트할 때마다 컴포넌트가 다시 렌더링됩니다. 컴포넌트가 다시 렌더링되면 그 내부의 코드가 처음부터 다시 실행됩니다.

매번ChatRoom컴포넌트가 다시 렌더링될 때마다 새로운options객체가 처음부터 생성됩니다. React는options객체가 지난 렌더링 동안 생성된options객체와다른 객체임을 인식합니다. 이것이 Effect(options에 의존함)를 다시 동기화하고, 타이핑할 때마다 채팅이 다시 연결되는 이유입니다.

이 문제는 객체와 함수에만 영향을 미칩니다. JavaScript에서는 새로 생성된 각 객체와 함수가 다른 모든 것과 구별되는 것으로 간주됩니다. 그 내부의 내용이 동일할지라도 상관없습니다!

객체와 함수 의존성은 Effect가 필요 이상으로 자주 재동기화되게 만들 수 있습니다.

이것이 가능할 때마다 Effect의 의존성으로 객체와 함수를 사용하지 않도록 노력해야 하는 이유입니다. 대신, 컴포넌트 외부로, Effect 내부로 옮기거나, 그들로부터 원시 값을 추출해 보세요.

정적 객체와 함수를 컴포넌트 외부로 이동하기

객체가 props나 state에 의존하지 않는다면, 해당 객체를 컴포넌트 외부로 이동할 수 있습니다:

이렇게 하면 린터에게 그것이 반응형 값이 아니라고증명할 수 있습니다. 재렌더링 결과로 변경될 수 없으므로 의존성이 될 필요가 없습니다. 이제ChatRoom을 재렌더링해도 Effect가 재동기화되지 않습니다.

이 방법은 함수에도 적용됩니다:

컴포넌트 외부에서 선언되었기 때문에createOptions는 반응형 값이 아닙니다. 이것이 Effect의 의존성에 지정될 필요가 없고, Effect가 재동기화되게 만들지도 않는 이유입니다.

동적 객체와 함수를 Effect 내부로 이동하기

객체가 재렌더링 결과로 변경될 수 있는 roomIdprop과 같은 반응형 값에 의존한다면, 컴포넌트외부로 꺼낼 수 없습니다. 그러나 그 생성 과정을 Effect 코드내부로 이동할 수는 있습니다:

이제 options가 Effect 내부에서 선언되었으므로, 더 이상 Effect의 의존성이 아닙니다. 대신, Effect가 사용하는 유일한 반응형 값은roomId입니다.roomId는 객체나 함수가 아니므로, 의도치 않게달라지지 않을 것이라고 확신할 수 있습니다. JavaScript에서 숫자와 문자열은 그 내용으로 비교됩니다:

이 수정 덕분에 입력을 편집해도 채팅이 더 이상 재연결되지 않습니다:

하지만 예상대로실제로 roomId드롭다운을 변경할 때는 다시 연결됩니다.

이 방법은 함수에도 적용됩니다:

Effect 내부의 로직을 그룹화하기 위해 자신만의 함수를 작성할 수 있습니다. 해당 함수들을 Effect내부에선언하기만 하면, 반응형 값이 아니므로 Effect의 의존성으로 포함할 필요가 없습니다.

객체에서 원시 값 읽기

때로는 props에서 객체를 받을 수 있습니다:

여기서의 위험은 부모 컴포넌트가 렌더링 중에 객체를 생성할 수 있다는 점입니다:

이렇게 되면 부모 컴포넌트가 다시 렌더링될 때마다 Effect가 다시 연결됩니다. 이 문제를 해결하려면 Effect외부에서객체에서 정보를 읽고, 객체와 함수 의존성을 피하세요:

로직이 약간 반복적일 수 있습니다(Effect 외부에서 객체의 일부 값을 읽은 다음, Effect 내부에서 동일한 값으로 객체를 생성합니다). 하지만 이렇게 하면 Effect가실제로어떤 정보에 의존하는지 매우 명확해집니다. 부모 컴포넌트가 의도하지 않게 객체를 다시 생성하더라도 채팅은 다시 연결되지 않습니다. 그러나options.roomId또는options.serverUrl이 실제로 다르다면 채팅은 다시 연결됩니다.

함수에서 원시 값 계산하기

동일한 접근 방식이 함수에도 적용될 수 있습니다. 예를 들어, 부모 컴포넌트가 함수를 전달한다고 가정해 보세요:

의존성으로 만들지 않으려면(그리고 다시 렌더링될 때 다시 연결되지 않도록 하려면) Effect 외부에서 호출하세요. 이렇게 하면 객체가 아닌roomIdserverUrl값을 얻을 수 있으며, Effect 내부에서 읽을 수 있습니다:

이 방법은 렌더링 중에 호출해도 안전한순수함수에만 적용됩니다. 함수가 이벤트 핸들러이지만 그 변경 사항이 Effect를 다시 동기화하지 않도록 하려면,대신 Effect Event로 감싸세요.

요약

  • 의존성은 항상 코드와 일치해야 합니다.
  • 의존성이 마음에 들지 않을 때 수정해야 하는 것은 코드입니다.
  • 린터를 억제하면 매우 혼란스러운 버그가 발생할 수 있으므로 항상 피해야 합니다.
  • 의존성을 제거하려면 린터에게 그것이 필요하지 않다는 것을 "증명"해야 합니다.
  • 특정 상호작용에 대한 응답으로 일부 코드가 실행되어야 한다면, 해당 코드를 이벤트 핸들러로 옮기세요.
  • Effect의 다른 부분들이 다른 이유로 다시 실행되어야 한다면, 여러 개의 Effect로 분리하세요.
  • 이전 상태를 기반으로 일부 상태를 업데이트하려면 업데이터 함수를 전달하세요.
  • 최신 값을 "반응" 없이 읽고 싶다면 Effect에서 Effect Event를 추출하세요.
  • JavaScript에서는 객체와 함수가 서로 다른 시점에 생성되었다면 서로 다른 것으로 간주됩니다.
  • 객체와 함수 의존성을 피하도록 노력하세요. 컴포넌트 외부나 Effect 내부로 옮기세요.

Try out some challenges

Challenge 1 of 4:Fix a resetting interval #

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.