v19.2Latest

이벤트와 Effect 분리하기

이벤트 핸들러는 동일한 상호작용을 다시 수행할 때만 재실행됩니다. 이벤트 핸들러와 달리, Effect는 props나 state 변수와 같이 읽는 값이 마지막 렌더링 때와 다르면 재동기화됩니다. 때로는 두 동작을 혼합한 형태도 원할 수 있습니다: 일부 값에는 반응하지만 다른 값에는 반응하지 않는 Effect를 말이죠. 이 페이지에서는 그 방법을 알려줍니다.

배울 내용
  • 이벤트 핸들러와 Effect 중 선택하는 방법
  • Effect는 반응형이고 이벤트 핸들러는 그렇지 않은 이유
  • Effect 코드의 일부가 반응형이 되지 않도록 하려면 어떻게 해야 하는지
  • Effect Event가 무엇이며 Effect에서 추출하는 방법
  • Effect Event를 사용하여 Effect에서 최신 props와 state를 읽는 방법

이벤트 핸들러와 Effect 중 선택하기

먼저, 이벤트 핸들러와 Effect의 차이점을 다시 살펴보겠습니다.

채팅방 컴포넌트를 구현한다고 상상해 보세요. 요구사항은 다음과 같습니다:

  1. 컴포넌트는 선택된 채팅방에 자동으로 연결되어야 합니다.
  2. "Send" 버튼을 클릭하면 채팅에 메시지를 보내야 합니다.

이미 코드를 구현했지만 어디에 배치해야 할지 확신이 서지 않는다고 가정해 보겠습니다. 이벤트 핸들러를 사용해야 할까요, 아니면 Effect를 사용해야 할까요? 이 질문에 답할 때마다코드가 실행되어야 하는 이유

이벤트 핸들러는 특정 상호작용에 응답하여 실행됩니다

사용자 관점에서 메시지 전송은 특정 "Send" 버튼이 클릭되었기 때문에 발생해야 합니다

이벤트 핸들러를 사용하면 sendMessage(message)가 사용자가 버튼을 누를 때실행된다는 것을 확신할 수 있습니다.

Effect는 동기화가 필요할 때마다 실행됩니다

컴포넌트를 채팅방에 연결된 상태로 유지해야 한다는 점을 상기해 보세요. 그 코드는 어디에 배치해야 할까요?

이 코드를 실행해야 하는이유는 특정 상호작용이 아닙니다. 사용자가 채팅방 화면으로 어떻게 또는 왜 이동했는지는 중요하지 않습니다. 사용자가 화면을 보고 상호작용할 수 있는 상태이므로, 컴포넌트는 선택된 채팅 서버에 연결된 상태를 유지해야 합니다. 채팅방 컴포넌트가 앱의 초기 화면이고 사용자가 아무런 상호작용도 수행하지 않았다고 해도, 여전히 연결해야 합니다. 이것이 Effect인 이유입니다:

이 코드를 사용하면 사용자가 수행한 특정 상호작용에 관계없이현재 선택된 채팅 서버에 항상 활성 연결이 있다는 것을 확신할 수 있습니다. 사용자가 앱을 열기만 했든, 다른 방을 선택했든, 다른 화면으로 이동했다가 돌아왔든, Effect는 컴포넌트가 현재 선택된 방과동기화 상태를 유지하고 필요할 때마다 재연결

반응형 값과 반응형 로직

직관적으로 말하면, 이벤트 핸들러는 버튼 클릭과 같이 항상 "수동으로" 트리거된다고 할 수 있습니다. 반면 Effect는 "자동적"입니다: 동기화 상태를 유지하는 데 필요한 만큼 자주 실행되고 재실행됩니다.

이를 생각하는 더 정확한 방법이 있습니다.

컴포넌트 본문 내부에서 선언된 props, state 및 변수를반응형 값

이러한 반응형 값들은 리렌더링으로 인해 변경될 수 있습니다. 예를 들어, 사용자가message를 수정하거나 드롭다운에서 다른roomId를 선택할 수 있습니다. 이벤트 핸들러와 Effect는 변경에 다르게 반응합니다:

  • 이벤트 핸들러 내부의 로직은반응형이 아닙니다.사용자가 동일한 상호작용(예: 클릭)을 다시 수행하지 않는 한 다시 실행되지 않습니다. 이벤트 핸들러는 반응형 값을 읽을 수 있지만 그 변경에 "반응"하지는 않습니다.
  • Effect 내부의 로직은반응형입니다.Effect가 반응형 값을 읽는다면,해당 값을 의존성으로 지정해야 합니다.그러면 리렌더링으로 인해 그 값이 변경될 때, React는 새로운 값으로 Effect의 로직을 다시 실행합니다.

이 차이점을 설명하기 위해 이전 예제를 다시 살펴보겠습니다.

이벤트 핸들러 내부의 로직은 반응형이 아닙니다

이 코드 줄을 살펴보세요. 이 로직은 반응형이어야 할까요, 아니면 아닐까요?

사용자의 관점에서,메시지 변경은message아닙니다메시지를 보내고 싶다는 의미가 아닙니다.이는 단지 사용자가 입력하고 있다는 의미일 뿐입니다. 다시 말해, 메시지를 보내는 로직은 반응형이어서는 안 됩니다. 반응형 값이 변경되었다는 이유만으로 다시 실행되어서는 안 됩니다. 그렇기 때문에 이 로직은 이벤트 핸들러에 속합니다:

이벤트 핸들러는 반응형이 아니므로,sendMessage(message)는 사용자가 전송 버튼을 클릭할 때만 실행됩니다.

Effect 내부의 로직은 반응형입니다

이제 이 줄들로 돌아가 보겠습니다:

사용자의 관점에서,방 ID 변경은roomId다른 방에 연결하고 싶다는 의미입니다.다시 말해, 방에 연결하는 로직은 반응형이어야 합니다. 여러분은 이 코드 줄들이반응형 값을 "따라잡아" 그 값이 다를 경우 다시 실행되기를원합니다. 그렇기 때문에 이 로직은 Effect에 속합니다:

Effect는 반응형이므로,createConnection(serverUrl, roomId)connection.connect()roomId의 각각 다른 값에 대해 실행됩니다. Effect는 채팅 연결을 현재 선택된 방과 동기화된 상태로 유지합니다.

Effect에서 비반응형 로직 추출하기

반응형 로직과 비반응형 로직을 혼합하려 할 때는 상황이 더 까다로워집니다.

예를 들어, 사용자가 채팅에 연결될 때 알림을 표시하려 한다고 가정해 보세요. 올바른 색상으로 알림을 표시하기 위해 props에서 현재 테마(어두운 모드 또는 밝은 모드)를 읽습니다:

그러나theme는 반응형 값입니다(리렌더링 결과로 변경될 수 있음). 그리고Effect에서 읽는 모든 반응형 값은 의존성으로 선언되어야 합니다.이제 Effect의 의존성으로theme을 지정해야 합니다:

이 예제를 실행해 보고 사용자 경험상 어떤 문제가 있는지 확인해 보세요:

하지만theme도 의존성이므로, 다크 테마와 라이트 테마를 전환할 때마다 채팅이또한다시 연결됩니다. 이는 좋지 않습니다!

다시 말해, 이 줄이 반응형 Effect(반응형) 안에 있더라도 이 줄이 반응형이 되기를원하지 않습니다:

이 비반응형 로직을 주변의 반응형 Effect와 분리할 방법이 필요합니다.

Effect Event 선언하기

특수 Hook인useEffectEvent를 사용하여 이 비반응형 로직을 Effect에서 추출하세요:

여기서onConnectedEffect Event라고 부릅니다. 이는 Effect 로직의 일부이지만, 이벤트 핸들러와 매우 유사하게 동작합니다. 그 내부의 로직은 반응형이 아니며, 항상 props와 state의 최신 값을 "확인"합니다.

이제 Effect 내부에서onConnectedEffect Event를 호출할 수 있습니다:

이렇게 하면 문제가 해결됩니다. Effect의 의존성 목록에서제거theme해야 했음을 주목하세요. 왜냐하면 이제 Effect에서 더 이상 사용되지 않기 때문입니다. 또한추가onConnected할 필요도 없습니다. 왜냐하면Effect Event는 반응형이 아니므로 의존성에서 제외해야 하기 때문입니다.

새로운 동작이 예상대로 작동하는지 확인하세요:

Effect Event는 이벤트 핸들러와 매우 유사하다고 생각할 수 있습니다. 주요 차이점은 이벤트 핸들러는 사용자 상호작용에 대한 응답으로 실행되는 반면, Effect Event는 Effect에서 여러분이 직접 트리거한다는 점입니다. Effect Event를 사용하면 Effect의 반응성과 반응형이어서는 안 되는 코드 사이의 "연결을 끊을" 수 있습니다.

Effect Event로 최신 props 및 state 읽기

Effect Event를 사용하면 의존성 린터 억제를 유혹할 수 있는 많은 패턴을 수정할 수 있습니다.

예를 들어, 페이지 방문을 기록하는 Effect가 있다고 가정해 보세요:

나중에 사이트에 여러 경로를 추가합니다. 이제Page컴포넌트는 현재 경로를 담은url prop을 받습니다. logVisit호출의 일부로url을 전달하고 싶지만, 의존성 린터가 불평합니다:

코드가 무엇을 하길 원하는지 생각해 보세요. 각 URL이 다른 페이지를 나타내므로 다른 URL에 대해 별도의 방문을 기록하기를원합니다. 다시 말해, 이 logVisit호출은url에 대해 반응형이 되어야 합니다. 따라서 이 경우에는 의존성 린터를 따르고 url을 의존성으로 추가하는 것이 합리적입니다:

이제 모든 페이지 방문과 함께 쇼핑 카트의 항목 수를 포함하고 싶다고 가정해 보겠습니다:

Effect 내부에서numberOfItems를 사용했으므로, 린터는 이를 의존성으로 추가하라고 요청합니다. 그러나logVisit호출이numberOfItems에 대해 반응형이 되길원하지 않습니다. 사용자가 쇼핑 카트에 무언가를 넣어numberOfItems가 변경된다고 해서, 사용자가 페이지를 다시 방문했다는 것을의미하지는 않습니다. 다시 말해, 페이지 방문은 어떤 의미에서 정확한 순간에 발생하는 "이벤트"입니다.

코드를 두 부분으로 나눕니다:

여기서onVisit은 Effect Event입니다. 그 안의 코드는 반응형이 아닙니다. 따라서 변경 시 주변 코드의 재실행을 걱정하지 않고numberOfItems(또는 다른 반응형 값!)를 사용할 수 있습니다.

반면, Effect 자체는 반응형으로 유지됩니다. Effect 내부의 코드는urlprop을 사용하므로, Effect는 다른url로 다시 렌더링될 때마다 다시 실행됩니다. 이는 차례로onVisitEffect Event를 호출합니다.

결과적으로, url이 변경될 때마다logVisit을 호출하고 항상 최신numberOfItems를 읽게 됩니다. 그러나numberOfItems가 자체적으로 변경되더라도 코드가 다시 실행되지는 않습니다.

참고

인수 없이 onVisit()을 호출하고 그 안에서url을 읽을 수 있을지 궁금할 수 있습니다:

이것은 작동하지만, 이 url을 Effect Event에 명시적으로 전달하는 것이 더 좋습니다.

Effect Events의 제한 사항

Effect Events는 사용 방법에 있어 매우 제한적입니다:

  • Effect 내부에서만 호출하세요.
  • 다른 컴포넌트나 Hook에 전달하지 마세요.

예를 들어, 다음과 같이 Effect Event를 선언하고 전달하지 마세요:

대신, Effect Events는 항상 이를 사용하는 Effect 바로 옆에 선언하세요:

Effect Events는 Effect 코드의 비반응적인 "조각"입니다. 이를 사용하는 Effect 옆에 위치해야 합니다.

요약

  • 이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행됩니다.
  • Effect는 동기화가 필요할 때마다 실행됩니다.
  • 이벤트 핸들러 내부의 로직은 반응적이지 않습니다.
  • Effect 내부의 로직은 반응적입니다.
  • Effect에서 비반응적 로직을 Effect Event로 옮길 수 있습니다.
  • Effect Events는 Effect 내부에서만 호출하세요.
  • Effect Events를 다른 컴포넌트나 Hook에 전달하지 마세요.

Try out some challenges

Challenge 1 of 4:Fix a variable that doesn’t update #

This Timer component keeps a count state variable which increases every second. The value by which it’s increasing is stored in the increment state variable. You can control the increment variable with the plus and minus buttons.

However, no matter how many times you click the plus button, the counter is still incremented by one every second. What’s wrong with this code? Why is increment always equal to 1 inside the Effect’s code? Find the mistake and fix it.