sohyeon kim

[React] 리액트 공식문서 정리 4(3) : 탈출구 - Effect 생명주기, 동기화, 의존성 배열, useEffect VS 이벤트 핸들러, useEffectEvent 본문

React

[React] 리액트 공식문서 정리 4(3) : 탈출구 - Effect 생명주기, 동기화, 의존성 배열, useEffect VS 이벤트 핸들러, useEffectEvent

aotoyae 2024. 12. 6. 14:44
728x90

 

 

💡 리액트 공식문서 정리 : 탈출구 - 반응형 Effect 의 생명주기, Effect 에서 이벤트 분리하기

 

1. 반응형 Effect 의 생명주기 : 동기화 시작 & 중지

Effect 는 컴포넌트와 다른 생명주기를 가진다. 컴포넌트는 마운트/업데이트/마운트 해제의 생명주기를 가지지만,

Effect 는 동기화를 시작하거나 중지하는 두 가지 작업만 할 수 있다.

 

다음 Effect 는 roomId prop 값에 의존한다. roomId 가 변경되면 Effect 가 다시 동기화(및 서버에 다시 연결)한다.

function ChatRoom({ roomId }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // 동기화 시작
    connection.connect();
    return () => connection.disconnect(); // 동기화 중지(cleanup 함수)
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>;
}

 

⚠️ 컴포넌트가 마운트될 때 동기화를 시작하고, 마운트 해제될 대 동기화를 중지할 것이라 생각할 수도 있다.

하지만 때로는 마운트된 상태에서 동기화를 여러 번 시작하고 중지해야 할 수도 있다.

 

이러한 동작이 필요한 이유와 발생 시기, 그리고 이러한 동작의 제어 방법에 대해 알아보자.

 

❗️참고❗️ 일부 effet 들은 cleanup 함수를 전혀 반환하지 않는다. 그럼 React 는 빈 cleanup 함수를 반환한 것처럼 동작한다.

 

1️⃣ 동기화가 두 번 이상 수행되어야 하는 이유

ChatRoom 컴포넌트가 사용자가 드롭다운에서 선택한 roomId prop 을 받는다고 가정해 보자.

처음 사용자가 'general' 대화방을 roomId 로 선택하면, 앱에 'general' 채팅방이 표시된다.

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

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

 

UI 가 표시되면 React 가 effect 를 실행해 동기화를 시작하고, 'general 방에 연결된다.

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

 

지금까진 괜찮다.

이후 사용자가 드롭다운에서 다른 방을 선택하고, React 는 먼저 UI 를 업데이트한다.

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

 

사용자는 UI 를 보고 'travel' 이 선택된 대화방임을 알 수 있다. 그러나 지난번에 실행된 effect 는 여전히 'general' 방에 연결되어 있다.

roomId prop 이 변경되었기 때문에 그 때 effect 가 수행한 작업('general' 방에 연결)이 현재 UI 와 일치하지 않게 된 것.

 

이 시점에서 React 는 두 가지 작업을 수행해야 한다.

  1. 이전 roomId 와의 동기화 중지('general' 방에서 연결 끊기)
  2. 새 roomId 와 동기화 시작('travel' 방에 연결)

다행히, 우린 이미 위 수행 방법을 React 에 알려줬다. effect 의 본문엔 동기화 시작 방법이 명시되어 있고,

cleanup 함수엔 동기화 중지 방법이 명시되어 있다. 이제 React 는 올바른 순서로 올바른 props 와 state 로 호출하기만 하면 된다.

 

2️⃣ React 가 effect 를 재동기화하는 방법

컴포넌트가 roomId prop 에 새로운 값('travel')을 받은 상태이고, 다른 방에 재연결하려면 React 가 effect 를 다시 동기화해야 한다.

 

동기화를 중지하기 위해 React 는 'general' 방에 연결한 후 effect 가 반환한 cleanup 함수를 호출한다.

roomId 가 'general' 이었기 때문에, cleanup 함수는 'general' 방에서 연결을 끊는다.

  return () => {
    connection.disconnect(); // "general" 방에서 연결 해제
  };

 

이제 React 는 렌더링 중에 우리가 제공한 effect 를 실행한다. 이번 roomId 인 'travel' 채팅방과 동기화가 시작된다.

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

 

이제 UI 에서 선택한 방과 동일한 방에 연결되었다! 또한, 컴포넌트가 다른 roomId 로 다시 렌더링할 때마다 effect 가 다시 동기화된다.

 

예시로 'travel' 에서 'music' 으로 변경한다면?

  • React 는 다시 cleanup 함수를 호출해 effect 동기화를 중지('travel' 방에서 연결 해제)한다.
  • 그 다음 새 roomId 로 본문을 실행해 동기화를 다시 시작('music' 방에 연결)한다.
  • 마지막으로 사용자가 다른 화면으로 이동하면 ChatRoom 이 마운트를 해제한다.
  • 이제 연결 상태를 유지할 필요가 없고, React 는 마지막으로 effect 동기화를 중지('music'에서 연결 해제)한다.

3️⃣ effect 의 관점에서 생각하기

ChatRoom 컴포넌트의 관점에서 일어난 일들을 요약해 보자.

  1. roomId 가 'general' 로 설정되어 마운트된 ChatRoom
  2. roomId 가 'travel' 로 설정되어 업데이트된 ChatRoom
  3. roomId 가 'music' 로 설정되어 업데이트된 ChatRoom
  4. 마운트 해제된 ChatRoom

컴포넌트의 생명주기에서 이러한 각 시점에서 effect 는 다른 작업을 수행했다.

  1. effect 가 'general' 방에 연결됨
  2. 'general' 방에서 연결이 끊어지고 'travel' 방에 연결된 effect
  3. 'travel' 방에서 연결이 끊어지고 'music' 방에 연결된 effect
  4. 'music' 방에서 연결이 끊어진 effect

이제 effect 자체의 관점에서 무슨 일이 일어났는지 살펴 보자.

  useEffect(() => {
    // roomId로 지정된 방에 연결된 effect...
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      // ...연결이 끊어질 때까지
      connection.disconnect();
    };
  }, [roomId]);

 

이 코드 구조는 어떤 일이 일어났는지, 겹치지 않는 시간의 연속으로 볼 수 있다.

  1. 'general' 방에 연결된 effect (연결이 끊어질 때까지)
  2. 'travel' 방에 연결된 effect (연결이 끊어질 때까지)
  3. 'music' 방에 연결된 effect (연결이 끊어질 때까지)

이전엔 컴포넌트의 관점에서 생각했으니, effect 를 '렌더링 후' 또는 '마운트 해제 전' 과 같은 특정 시점에 실행되는 '콜백' 또는 '생명주기 이벤트' 로 생각하기 쉬웠다. 이러한 사고는 매우 복잡해지므로 피하는 것이 좋다.

 

대신 항상 한 번에 하나의 시작/중지 사이클에만 집중하자. 컴포넌트를 마운트, 업데이트 또는 마운트 해제하는 것은 중요하지 않다.

동기화 시작 방법과 중지 방법만 설명하면 된다. 이 작업을 잘 수행하면 필요 횟수만큼 effect 를 시작하고 중지할 수 있다.

 

4️⃣ 각 effect 는 별도의 동기화를 프로세스를 나타낸다.

위 로직은 이미 작성한 effect 와 동시에 실행되어야 하므로 관련 없는 로직을 effect 에 추가하지 말자.

예시로 사용자가 회의실을 방문할 때 분석 이벤트를 전송하고 싶다고 가정해 보자.

이미 roomId 에 의존하는 effect 가 있으므로 거기에 분석 호출을 추가하고 싶을 수 있다.

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

 

하지만 나중에 이 effect 에 연결을 다시 설정해야 하는 다른 종속성을 추가한다고 가정해 보자.

이 effect 가 다시 동기화되면 의도하지 않은 동일한 방에 대해 logVisit(roomId) 도 호출한다.

방문 기록은 연결과는 다른 별개의 프로세스이다. 두 개의 개별 effect 로 작성하자.

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

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

 

위 예시에선 한 effect 를 삭제해도 다른 effect 의 로직이 깨지지 않는다. 이는 서로 다른 것을 동기화하므로 분리하는 것이 합리적이라는 것을 알려준다. 반면 일관된 로직을 별도의 effect 로 분리하면 코드가 더 깔끔해 보일 순 있지만 유지 보수가 어려워진다.

➡️ 따라서 코드가 더 깔끔해 보이는지 여부가 아니라 프로세스가 동일하거나, 분리되어 있는지를 고려해야 한다.

 

5️⃣ 반응형 값에 '반응'하는 effect

위 예시의 effect 에서 두 개의 변수(serverUrl & roomId)를 읽지만 의존성 배열에 roomId 만 지정했다.

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

 

serverUrl 이 의존성 배열에 포함될 필요가 없는 이유는 무엇일까? 리렌더링으로 인해 serverUrl 이 변경되지 않기 때문이다!

컴포넌트가 몇 번을 리렌더링하든 항상 동일하다. serverUrl 은 절대 변경되지 않으므로 의존성 배열에 포함하는 것은 의미가 없다.

의존성 배열의 요소는 시간이 지남에 따라 변경될 때만 무언가를 수행한다.

 

반면 roomId 는 리렌더링 시 달라질 수 있다.

컴포넌트 내부에 선언된 props, state 및 기타값은 렌더링 중에 계산되고, React 데이터 흐름에 참여하기 때문에 반응형 값이다.

serverUrl 이 state 변수라면 반응형일 것이다.(반응형 값은 의존성 배열에 포함되어야 한다.)

serverlUrl 을 의존성 배열에 포함하면 effect 가 변경된 후 다시 동기화되도록 할 수 있다.

function ChatRoom({ roomId }) { // Props는 시간이 지남에 따라 변화합니다.
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // State는 시간이 지남에 따라 변화합니다.

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // effect는 Props와 state를 읽습니다.
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, [roomId, serverUrl]); // 따라서 이 effect는 props와 state에 "의존"한다고 React에 알려줍니다.
  // ...
}

 

 

6️⃣ 빈 의존성 배열이 있는 effect 의 의미

serverUrl 과 roomId 를 모두 컴포넌트 외부로 이동하면 어떻게 될까?

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

function ChatRoom() {
  useEffect(() => {
    // ...
  }, []); // ✅ 선언된 모든 종속성
  // ...
}

 

이제 effect 의 코드는 어떤 반응형 값도 사용하지 않으므로 의존성 배열이 비어있다.

컴포넌트 관점에서 생각해 보면 빈 의존성 배열은 이 effect 가 컴포넌트가 마운트될 때만 채팅방에 연결되고 마운트 해제될 때만 연결이 끊어진다는 것을 의미한다.(React 는 로직 테스트를 위해 개발 단계에서 한 번 더 동기화한다.)

 

하지만 effcet 관점으로 보면 마운트 및 마운트 해제에 대해 전혀 생각할 필요가 없다. 중요한 것은 effect 가 동기화를 시작하고 중지하는 작업을 지정한 것이다. 현재는 반응형 종속성이 없다. 하지만 사용자가 roomId 또는 serverUrl 을 변경하려는 경우(반응형이 되는 경우) effect 의 코드는 변경되지 않는다. 종속성에 추가하기만 하면 된다.

 

7️⃣ 컴포넌트 내부의 모든 값 - props, state, 변수 는 반응형 : 모든 반응형 값은 리렌더링 시 변경될 수 있으니 종속 요소로 포함해야 한다.

props 와 state 만 반응형 값인 것은 아니다. 이들로부터 계산하는 값도 반응형이다. props 나 state 가 변경되면 컴포넌트가 리렌더링되고, 그로부터 계산된 값도 변경된다. 이 때문에 effect 에서 사용하는 컴포넌트 본문의 모든 변수는 effect 종속성 목록에 있어야 한다.

 

❗️ 즉, effect 는 컴포넌트 본문의 모든 값에 '반응'한다.

 

사용자가 드롭다운에서 채팅 서버를 선택할 수 있지만 설정에서 기본 서버를 구성할 수도 있다고 가정해 보자.

이미 settings state 를 context 에 넣어서 해당 context 에서 settings 를 읽었고, props 에서 선택한 서버와 기본 서버를 기준으로 serverUrl 을 계산한다.

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

 

이 예시에서 serverUrl 은 prop 이나 state 변수가 아니다. 렌더링 중 계산하는 일반 변수이다.

하지만 렌더링 중 계산되므로 리렌더링 시 변경될 수 있다. 그러므로 반응형 값이다.

 

🧐 전역 or 변경할 수 있는 값은?

변경 가능한 값(전역 변수 포함)은 반응하지 않는다.

location.pathname 과 같은 변경 가능 값은 종속성이 될 수 없다. 이 값은 변경 가능하므로 React 렌더링 데이터 흐름 외부에서 언제든 변경할 수 있지만, 이 값을 변경해도 컴포넌트가 다시 렌더링되지는 않는다. 따라서 종속성에서 지정했더라도 React 는 effect 가 변경될 때 effect 를 다시 동기화할 지 알 수 없다. 또한 렌더링 도중(의존성을 계산할 때) 변경 가능한 데이터를 읽는 것은 렌더링의 순수성을 깨드리기에 React 의 규칙을 위반한다. 대신, useSyncExternalStore 를 사용해 외부 변경 가능 값을 읽고 구독해야 한다.

 

ref.current 와 같이 변경 가능한 값이나 이 값에서 읽은 것 역시 종속성이 될 수 없다. useRef 가 반환하는 ref 객체 자체는 종속성이 될 수있지만, current prop 은 의도적으로 변경할 수 있다. 이를 통해 리렌더링을 트리거하지 않고도 무언가를 추적할 수 있다. 하지만 변경해도 다시 렌더링이 트리거되지 않기 때문에 반응형 값이 아니며, React 는 이 값이 변경될 때 effect 를 다시 실행할 지 알지 못한다.

 

➕ 린터는 이러한 문제를 자동으로 확인한다.

 

8️⃣ React 는 모든 반응형 값을 종속성으로 지정했는지 확인한다.

린터가 React 에 대해 구성됐다면, effect 의 코드에서 사용되는 모든 반응형 값이 종속성에 지정되었는지 확인한다.

예시로 roomId 와 serverUrl 이 모두 반응형이기에 아래 코드는 린트 오류가 일어난다.

 

이것은 React 오류처럼 보일 수 있지만, 코드의 버그를 지적하는 것이다.

roomId 와 serverUrl 은 시간이 지남에 다라 변경될 수 있지만, 변경 시 effect 를 다시 동기화하는 것을 잊어버리고 있다.

(사용자가 UI 에서 다른 값을 선택해도 초기 roomId 와 serverUrl 에 연결된 상태로 유지된다.)

 

버그를 수정하려면 린터의 제안에 따라 effect 의 의존성 배열에 roomId 및 serverUrl 을 지정하자.

 }, [serverUrl, roomId]); // ✅ 선언된 모든 종속성

 

❗️ 어떤 경우에선 컴포넌트 내부에 값이 선언되더라도 절대 변하지 않는다는 것을 React 가 알고 있다.

예시로, useState 에서 반환되는 set 함수와 useRef 에서 반환되는 ref 객체는 안정적이며, 다시 렌더링해도 변경되지 않도록 보장된다.

안정된 값은 반응하지 않으므로(변경되지 않으므로) 목록에서 생략할 수 있다.

 

9️⃣ 다시 동기화하지 않으려는 경우 어떻게 해야 할까?

위 예시에선 roomId 와 serverUrl 을 의존성 배열에 넣어 린트 오류를 해결했다.

이 방법 대신 이러한 값이 반응형 값이 아니라는 것, 즉 리렌더링으로 변경되지 않는 다는 것을 린터에 '증명'할 수도 있다.

예시로, roomId 와 serverUrl 이 렌더링에 의존하지 않고 항상 같은 값을 갖는다면 컴포넌트 외부로 옮길 수 있다.

const serverUrl = 'https://localhost:1234'; // serverUrl는 반응형이 아닙니다.
const roomId = 'general'; // roomId는 반응형이 아닙니다.

function ChatRoom() {
  useEffect(() => {
    // ...
  }, []); // ✅ 선언된 모든 종속성
  // ...
}

 

이제 종속성이 될 필요가 없다. 아니면 effect 내부로 이동시킬 수도 있다. 이는 렌더링 중 계산되지 않으므로 반응하지 않는다.

function ChatRoom() {
  useEffect(() => {
    const serverUrl = 'https://localhost:1234'; // serverUrl는 반응형이 아닙니다.
    const roomId = 'general'; // roomId는 반응형이 아닙니다.
    // ...
  }, []); // ✅ 선언된 모든 종속성
  // ...
}

 

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

 

종속성을 '선택'할 수 없다. 종속성엔 effect 에서 읽은 모든 반응형 값이 포함되어야 한다.(린터가 이를 강제한다.) 때때로 이에 따라 무한 루프와 같은 문제가 발생하거나 effect 가 너무 자주 재동기화될 수 있다. 하지만 그렇다고 린터를 억제하지 말고, 아래 방법을 시도해 보자.

    • effect 가 독립적인 동기화 프로세스를 나타내는지 확인하라. effect 가 아무것도 동기화하지 않는다면 불필요한 것일 수 있다. 여러 개의 독립적인 것을 동기화한다면 분할하자.
    • props 나 state 에 '반응'하지 않고, effect 를 다시 동기화하지 않고 최신 값을 읽으려면 effect 를 반응하는 부분(effect 에 유지할 것)과 반응하지 않는 부분(effect 이벤트라고 하는 것으로 추출할 수 있는 것)으로 분리하면 된다. 🔗
    • 객체와 함수를 종속성으로 사용하지 말자. 렌더링 중 오브젝트와 함수를 생성한 다음 effect 에서 읽으면 렌더링할 때마다 오브젝트와 함수가 달라진다. 그러면 매번 effect 를 다시 동기화해야 한다. 🔗

 

2. Effect 에서 이벤트 분리하기 : 일부(불필요한) 값이 Effect 를 다시 발생시키는 것을 막는 방법 -Event 로 옮기자!

🔧 개발 중 : 이 섹션에선 아직 안정된 버전의 React 로 출시되지 않은 실험적인 API 에 대해 설명한다.

 

이벤트 핸들러는 같은 상호작용을 반복하는 경우에만 다시 실행되고, Effect 는 이벤트 핸들러와 달리 prop 이나 state 변수 등 읽은 값이 마지막 렌더링 때와 다르면 다시 동기화한다. 때로는 두 동작이 섞여 어떤 값에는 반응해 다시 실행되지만, 다른 값에는 그러지 않는 Effect 를 원할 때도 있다. 이를 effect 와 이벤트로 분리하자.

 

아래 예시에서 effect 내 모든 코드는 반응형이며, 읽은 반응형 값이 리렌더링으로 인해 변경되면 다시 실행된다.

(roomId 또는 theme 가 변경되면 채팅에 다시 연결된다.)

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

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

function ChatRoom({ roomId, theme }) {
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      showNotification('Connected!', theme);
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId, theme]);

  return <h1>Welcome to the {roomId} room!</h1>
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [isDark, setIsDark] = 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>
      <label>
        <input
          type="checkbox"
          checked={isDark}
          onChange={e => setIsDark(e.target.checked)}
        />
        Use dark theme
      </label>
      <hr />
      <ChatRoom
        roomId={roomId}
        theme={isDark ? 'dark' : 'light'}
      />
    </>
  );
}

 

이것은 이상적이지 않다. roomId 가 변경된 경우에만 채팅에 다시 연결하고 싶다. theme 를 전환해도 채팅에 다시 연결되지 않아야 한다.

theme 를 읽는 코드를 Effect 에서 Effect Event 로 옮기자.

import { useState, useEffect } from 'react';
import { experimental_useEffectEvent as useEffectEvent } from 'react';
import { createConnection, sendMessage } from './chat.js';
import { showNotification } from './notifications.js';

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

function ChatRoom({ roomId, theme }) {
  const onConnected = useEffectEvent(() => {
    showNotification('Connected!', theme);
  });

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.on('connected', () => {
      onConnected();
    });
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return <h1>Welcome to the {roomId} room!</h1>
}

 

Effect 이벤트 내부의 코드는 반응이 아니므로 theme 를 변경해도 더 이상 Effect 가 다시 연결하지 않는다.

자세히 🔗

 

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

먼저 이벤트 핸들러와 Effect 의 차이점에 대해 간단히 알아보자. 채팅방 컴포넌트를 구현한다면 요구사항은 아래와 같다.

  1. 채팅방 컴포넌트는 선택된 채팅방에 자동으로 연결되어야 한다.
  2. '전송' 버튼을 클릭하면 채팅에 메세지를 전송해야 한다.

이 코드들이 이미 구현되었을 때, 이벤트 핸들러와 Effect 중 어디에 넣어야 할까?

해당 코드가 실행되어야 하는 이유를 고려해 보자.

 

1️⃣ 이벤트 핸들러는 특정 상호작용에 대한 응답으로 실행된다.

사용자 관점에서 메시지는 '전송' 버튼이 클릭되었기 때문에 전송되어야 한다. 그러므로 메시지를 전송하는 건 이벤트 핸들러가 되어야 한다.

이벤트 핸들러는 특정 상호작용을 처리하게 해주고, 사용자에게 버튼을 누를 때만 sendMessage(message) 가 실행된다고 알려준다.

function ChatRoom({ roomId }) {
  const [message, setMessage] = useState('');
  // ...
  function handleSendClick() {
    sendMessage(message);
  }
  // ...
  return (
    <>
      <input value={message} onChange={e => setMessage(e.target.value)} />
      <button onClick={handleSendClick}>전송</button>
    </>
  );
}

 

2️⃣ Effect 는 동기화가 필요할 때마다 실행된다.

채팅방 컴포넌트는 채팅방과의 연결을 유지해야 한다는 요구사항을 떠올려 보자. 이 코드를 실행하는 이유는 어떤 특정 상호작용 때문이 아니다. 사용자가 채팅방 화면으로 이동한 이유나 방법은 상관없다. 사용자가 현재 채팅방 화면을 보고 상호작용할 수 있으므로 컴포넌트는 선택된 채팅 서버에 계속 연결되어 있어야 한다. 채팅방 컴포넌트가 앱의 첫 화면이고 사용자가 아무런 상호작용을 하지 않은 경우라 해도 여전히 연결되어 있어야 한다. 그러므로 이 코드는 Effect 이다.

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

 

이제 사용자가 수행하는 특정 상호작용에 상관없이 현재 선택된 채팅 서버와 항상 연결된 상태임을 확신할 수 있다.

사용자가 앱을 열기만 했든, 다른 방을 선택했든, 다른 화면으로 이동했다가 다시 돌아왔든, 컴포넌트가 현재 선택된 방과 동기화된 상태를 유지할 것이며 필요할 때마다 다시 연결할 것을 Effect 가 보장한다.

 

반응형 값과 반응형 로직

이벤트 핸들러는 버튼 클릭과 같이 항상 '수동으로' 트리거되지만, Effect 는 동기화 유지에 필요한 만큼 자주 실행/재실행되기 때문에 '자동으로' 트리거된다고 말할 수 있다. 이에 대해 더 자세히 알아보자.

 

컴포넌트 본문 내부에 선언된 props, state, 변수를 반응형 값이라 한다. 다음 예시에서 serverUrl 은 반응형 값이 아니지만 roomId 와 message 는 반응형 값이다. 이 반응형 값은 데이터 렌더링 과정에 관여한다.

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

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

  // ...
}

 

이러한 반응형 값은 리렌더링으로 변경될 수 있다. 예시로 사용자가 message 를 편집하거나 드롭다운에서 다른 roomId 를 선택하는 경우가 있다. 이 때 이벤트 핸들러와 Effect 는 다르게 반응한다.

  • 이벤트 핸들러 내부의 로직은 반응형이 아니다. 사용자가 같은 상호작용을 반복하지 않는 한 재실행되지 않는다. 이벤트 핸들러는 변화에 '반응'하지 않으면서 반응형 값을 읽을 수 있다.
  • Effect 내부의 로직은 반응형이다. Effect 에서 반응형 값을 읽는 경우 그 값을 의존성으로 지정해야 한다. 그렇게 하면 리렌더링이 그 값을 바꾸는 경우 React 가 새로운 값으로 Effect 의 로직을 다시 실행한다.

이전 예시를 통해 이 차이에 대해 알아 보자.

 

1️⃣ 이벤트 핸들러 내부 로직은 반응형이 아니다.

    // ...
    sendMessage(message);
    // ...

 

위 로직은 반응형일까, 아닐까?

사용자 관점에서 message 를 바꾸는 것은 메시지를 전송하고 싶다는 의미는 아니다. 사용자가 입력 중이라는 의미일 뿐이다.

즉 메시지를 전송하는 로직은 반응형이어선 안된다. 반응형 값이 변경되었단 이유만으로 로직이 재실행되어선 안된다. 그러므로 이 로직은 이벤트 핸들러에 속한다.

  function handleSendClick() {
    sendMessage(message);
  }

 

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

 

2️⃣ Effect 내부의 로직은 반응형이다.

    // ...
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    // ...

 

위 로직은 반응형일까, 아닐까?

사용자 관점에서 roomId 를 바꾸는 것은 다른 방에 연결하고 싶다는 의미이다. 즉 방에 연결하기 위한 로직은 반응형이어야 한다.

우리는 이 코드가 반응형 값을 '따라가고' 그 값이 바뀌면 다시 실행되기를 원한다. 그러므로 이 로직은 Effect 에 속한다.

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

 

Effect 는 반응형이므로 createConnection(serverUrl, roomId) 와 connection.connect() 는 구별되는 모든 roomId 값에 대해 실행될 것이다. Effect 는 채팅 연결과 현재 선택된 방의 동기화를 유지해 준다.

 

 

 

🔗 https://ko.react.dev/learn/lifecycle-of-reactive-effects

 

반응형 effects의 생명주기 – React

The library for web and native user interfaces

ko.react.dev

 

 

 

728x90
반응형