본문 바로가기
React-study/dil

[DIL] useEffect

by 어느새벽 2024. 6. 4.

useEffect는 컴포넌트를 외부 시스템과 동기화할 수 있는 리액트 훅이다.

useEffect(setup, dependencies?)

 

컴포넌트의 최상위 레벨에서 useEffect를 호출하여 Effect를 선언하다.

 

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

 

매개변수

  • setup: Effect의 로직이 포함된 함수이다. 셋업함수는 선택적으로 클린업 함수를 반환할 수도 있다. React는 컴포넌트가 DOM에 추가되면 셋업함수를 실행한다. 의존성이 변경되어 다시 렌더링할 때마다 React는 (클린업 함수가 있는 경우) 먼저 이전 값으로 클린업 함수를 실행한 다음, 새 값으로 셋업 함수를 실행한다. 컴포넌트가 DOM에서 제거되면, 리액트는 마지막으로 클린업 함수를 실행한다.
  • 선택적 의존성( optional dependencies ): 셋업코드 내에서 참조된 모든 반응형 값의 목록이다. 반응형 값은 props, state, 컴포넌트 본문 내부에서 직접 선언한 모든 변수와 함수를 포함한다. 리액트용으로 구성된 린터는 모든 반응형 값이 의존성에 잘 지정되었는지 확인한다. 의존성 목록에는 고정된 수의 항목이 있어야 하며 [dep1, dep2, dep3]과 같이 인라인으로 작성해야 한다. 리액트는 각 의존성에 대해 Object.is로 이전 값과 비교한다. 의존성을 전혀 지정하지 않으면 컴포넌트를 다시 렌더링할 때마다 Effect가 다시 실행된다. 의존성 배열을 전달할 때, 빈 배열을 전달할 때, 그리고 의존성을 전혀 전달하지 않을 때의 차이를 확인하는 것이 도움이 된다.

반환값

useEffect는  undefined를 반환한다.

 

주의사항

  • useEffect는 훅이므로 컴포넌트의 최상위 레벨 또는 자체 훅에서만 호출할 수 있다. 반복문이나 조건문 내부에서는 호출 할 수 없다. 필요한 경우 새 컴포넌트를 추출하고 state를 그 안으로 옮겨야 한다.
  • 외부 시스템과 동기화하려는 목적이 아니라면 Effect가 필요하지 않을 수도 있다.
  • Strict모드가 켜져 있으면 리액트는 첫번째 실세 셋업 전에 개발 전용의 셋업 + 클린업 사이클을 한번 더 실행한다. 이는 클린업 로직이 셋업 로직을 "미러링"하고 셋업이 수행 중인 모든 작업을 중지하거나 취소하는지를 확인하는 스트레스 테스트이다. 문제가 발생하면 클린업 기능을 구현해야 한다.
  • 의존성 중 일부가 컴포넌트 내부에 정의된 객체 또는 함수인 경우  Effect가 필요 이상으로 자주 다시 실행될 위험이 있다. 이 문제를 해결하려면 불필요한 객체 및 함수 의존성을 제거해야 한다. 혹은 Effect 외부에서 state 업데이트 추출 및 비반응형 로직을 제거할 수도 있다.
  • Effect가 상호작용(ex. 클릭)으로 인한 것이 아니라면, 리액트는 브라우저가 Effect를 실행하기 전에 업데이트된 화면을 먼저 그리도록 한다. Effect가 시각적인 작업(ex.툴팁 위치 지정)을 하고 있고, 지연이 눈에 띄는 경우(ex. 깜박임), useEffect를 useLayoutEffect로 대체해야 한다.
  • Effects는 클라이언트에서만 실행된다. 서버 렌더링 중에는 실행되지 않는다.

사용법

외부 시스템에 연결하기 

때로는 컴포넌트가 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 연결 상태를 유지해야 할 수도 있다. 이러한 시스템은 리액트에서 제어되지 않으므로 외부라고 한다.

컴포넌트를 외부 시스템에 연결하려면 컴포넌트의 최상위 레벨에서 useEffect를 호출해야 한다.

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

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

 

useEffect에는 두 개의 인자를 전달한다.

  • 해당 시스템에 연결하는 셋업 코드가 포함된 셋업함수. 해당 시스템과 연결을 끊는 클린업 코드가 포함된 클린업 함수 반환
  • 해당 함수 내부에서 사용되는 컴포넌트의 모든 값을 포함한 의존성 목록

React는 필요할 때마다 셋업 및 클린업 함수를 호출하는데, 이는 여러 번 발생할 수 있다.

 

 1. 컴포넌트가 페이지에 추가될 때(마운트)마다 셋업 코드를 실행한다.

 2. 의존성이 변경된 컴포넌트를 다시 렌더링할 때마다 

  • 먼저 이전 props와 state로 클린업 코드를 실행한다.
  • 그런 다음 새 props와 state로 셋업 코드를 실행한다.

 3. 컴포넌트가 페이지에서 제거되면 (마운트 해제) 마지막으로 한번 클린업 코드를 실행한다.

 

위의 ChatRoom 컴포넌트가 페이지에 추가되면 초기 sevarUrl 및 roomId로 채팅방에 연결된다. 다시 렌더링한 결과 serverUrl 또는 roomId가 변경되면 (ex.사용자가 드롭다운에서 다른 채팅방을 선택하는 경우) Effect는 이전 채팅방과의 연결을 끊고 다음 채팅방에 연결한다. ChatRoom 컴포넌트가 페이지에서 제거되면 Effect는 마지막으로 연결을 끊는다.

 

버그를 찾는 데 도움을 주기 위해 개발 환경에서 리액트는 실제셋업 전에 셋업 및 클린업을 한번 더 실행한다. 이는 Effect의 로직이 올바르게 구현되었는지 확인하는 스트레스 테스트이다. 이로 인해 눈에 보이는 문제가 발생하면 클린업 함수에 일부 로직이 누락된 것이다. 클린업 함수는 셋업 함수가 수행하던 작업을 중지하거나 취소해야 한다. 사용자 경험상 상용에서 셋업이 한번 호출되는 것과 개발 환경에서 셋업 -> 클린업 -> 셋업 순서로 호출되는 것을 구분할 수 없어야 한다. 

 

모든 Effect를 독립적인 프로세스로 작성하고 한 번에 하나의 셋업/클린업 주기만 생각하세요.

컴포넌트가 마운트, 업데이트, 마운트 해제 중 어느 단계에 있는지는 중요하지 않다. 클린업 로직이 셋업 로직을 올바르게 '미러링'하고 있다면, 필요한 만큼 자주 셋업과 클린업을 실행하더라도 Effect는 탄력적으로 작동한다.

 

커스텀 훅으로 Effect 감싸기

Effect는 "탈출구"이다. "리액트를 벗어나야 할 때", 또는 더 나은 빌트인 솔류션이 없을 때 사용한다. Effect를 수동으로 ㅣ작성해야 하는 경우가 자주 발생한다면 이는 컴포넌트가 의존하는 일반적인 동작에 대한 커스텀 훅을 추출해야 한다는 신호일 수 있다. 

 

예를들어, 이 useChatRoom 커스텀 훅은 Effect의 로직을 보다 선언적인 API 뒤에 "숨긴다"

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

그러면 모든 컴포넌트에서 이와 같이 사용할 수 있다. 

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });
  // ...

이밖에도 리액트 생태계에는 다양한 목적에 맞는 훌륭한 커스텀 훅이 많이 있다. 

 

아래는 이전 예제 중 하나와 동일하지만 로직을 커스텀 훅으로 추출한 것이다.

// App.js

import { useState } from 'react';
import { useChatRoom } from './useChatRoom.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useChatRoom({
    roomId: roomId,
    serverUrl: serverUrl
  });

  return (
    <>
      <label>
        Server URL:{' '}
        <input
          value={serverUrl}
          onChange={e => setServerUrl(e.target.value)}
        />
      </label>
      <h1>Welcome to the {roomId} room!</h1>
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  const [show, setShow] = 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>
      <button onClick={() => setShow(!show)}>
        {show ? 'Close chat' : 'Open chat'}
      </button>
      {show && <hr />}
      {show && <ChatRoom roomId={roomId} />}
    </>
  );
}
// useChatRoom.js

import { useEffect } from 'react';
import { createConnection } from './chat.js';

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

export function createConnection(serverUrl, roomId) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

 

React가 아닌 위젯 제어하기 

외부 시스템을 컴포넌트의 특정 prop이나 state와 동기화하고 싶을 때가 있다. 예를 들면 리액트 없이 작성된 타사 맵 위젯이나 비디오 플레이어 컴포넌트가 있는 경우, Effect를 사용하여 해당 state를 리액트 컴포넌트의 현재 state와 일치시키는 메서드를 호출할 수 있다. 이 Effect는 map-widget.js에 정의된 MapWidget 클래스의 인스턴스를 생성한다. Map 컴포넌트의 zoomLevel prop을 변경하면  Effect는 클래스 인스턴스에서 setZoom()을 호출하여 동기화 상태를 유지한다.

// App.js

import { useState } from 'react';
import Map from './Map.js';

export default function App() {
  const [zoomLevel, setZoomLevel] = useState(0);
  return (
    <>
      Zoom level: {zoomLevel}x
      <button onClick={() => setZoomLevel(zoomLevel + 1)}>+</button>
      <button onClick={() => setZoomLevel(zoomLevel - 1)}>-</button>
      <hr />
      <Map zoomLevel={zoomLevel} />
    </>
  );
}

// Map.js

import { useRef, useEffect } from 'react';
import { MapWidget } from './map-widget.js';

export default function Map({ zoomLevel }) {
  const containerRef = useRef(null);
  const mapRef = useRef(null);

  useEffect(() => {
    if (mapRef.current === null) {
      mapRef.current = new MapWidget(containerRef.current);
    }

    const map = mapRef.current;
    map.setZoom(zoomLevel);
  }, [zoomLevel]);

  return (
    <div
      style={{ width: 200, height: 200 }}
      ref={containerRef}
    />
  );
}

// map-widget.js

import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export class MapWidget {
  constructor(domNode) {
    this.map = L.map(domNode, {
      zoomControl: false,
      doubleClickZoom: false,
      boxZoom: false,
      keyboard: false,
      scrollWheelZoom: false,
      zoomAnimation: false,
      touchZoom: false,
      zoomSnap: 0.1
    });
    L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '© OpenStreetMap'
    }).addTo(this.map);
    this.map.setView([0, 0], 0);
  }
  setZoom(level) {
    this.map.setZoom(level);
  }
}

이 예제에서는 MapWidget 클래스가 자신에게 전달된 DOM 노드만 관리하기 때문에 클린업 함수가 필요하지 않다. Map React 컴포넌트가 트리에서 제거된 후, DOM 노드와 MapWidget 클래스 인스턴스는 브라우저 JavaScript 엔진에 의해 자동으로 가비지컬렉팅 된다.

 

Effect로 데이터 페칭하기 

Effect를 사용하여 컴포넌트에 대한 데이터를 페치할 수 있다. 프레임워크를 사용하는 경우 프레임워크의 데이터 페칭 메커니즘을 사용하는 것이 Effects를 수동으로 작성하는 것보다 훨씬 효율적이다. 

 

Effect에서 데이터를 수동으로 페치하려는 경우 코드는 다음과 같다. 

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);

  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    };
  }, [person]);

  // ...

ignore 변수는 false로 초기화되고 클린업 중에 true로 설정된다. 이렇게 하면 네트워크 응답이 보낸 순서와 다른 순서로 도착하더라도 '조건경합'이 발생하지 않는다.

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    let ignore = false;
    setBio(null);
    fetchBio(person).then(result => {
      if (!ignore) {
        setBio(result);
      }
    });
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

async / await 구문을 사용하여 다시 작성할 수도 있지만 그래도 클린업 함수는 제공해야 한다.

 

import { useState, useEffect } from 'react';
import { fetchBio } from './api.js';

export default function Page() {
  const [person, setPerson] = useState('Alice');
  const [bio, setBio] = useState(null);
  useEffect(() => {
    async function startFetching() {
      setBio(null);
      const result = await fetchBio(person);
      if (!ignore) {
        setBio(result);
      }
    }

    let ignore = false;
    startFetching();
    return () => {
      ignore = true;
    }
  }, [person]);

  return (
    <>
      <select value={person} onChange={e => {
        setPerson(e.target.value);
      }}>
        <option value="Alice">Alice</option>
        <option value="Bob">Bob</option>
        <option value="Taylor">Taylor</option>
      </select>
      <hr />
      <p><i>{bio ?? 'Loading...'}</i></p>
    </>
  );
}

Effects에서 직접 데이터를 페칭하는 작업을 반복적으로 작성하면 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기가 어려워진다. 직접 만들거나 커뮤니티에서 유지 관리하는 커스텀 훅을 사용하는 것이 더 쉽다.

 

반응형 의존성 지정

Effrect의 의존성을 "선택"할 수 없다는 점에 유의해야한다. Effect의 코드에서 사용되는 모든 반응형 값은 의존성으로 선언해야 한다. Effect의 의존성 목록은 주변 코드에 의해 결정된다.

function ChatRoom({ roomId }) { // This is a reactive value
  const [serverUrl, setServerUrl] = useState('https://localhost:1234'); // This is a reactive value too

  useEffect(() => {
    const connection = createConnection(serverUrl, roomId); // This Effect reads these reactive values
    connection.connect();
    return () => connection.disconnect();
  }, [serverUrl, roomId]); // ✅ So you must specify them as dependencies of your Effect
  // ...
}

 

serverUrl 또는 roomId가 변경되면 Effect는 새 값을 사용하여 채팅에 다시 연결한다. 

 

반응형 값에는 props와 컴포넌트 내부에서 직접 선언된 모든 변수, 함수가 포함된다. roomId와 serverUrl은 반응형 값이기 때문에 의존성 목록에서 제거할 수 없다. 만약 이 값을 생략하려고 할 때 린터가 리액트용으로 올바르게 구성되어 있다면 린터는 이를 수정해야 하는 실수로 표시해준다.

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');
  
  useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => connection.disconnect();
  }, []); // 🔴 React Hook useEffect has missing dependencies: 'roomId' and 'serverUrl'
  // ...
}

 

의존성을 제거하려면 의존성이여야 할 필요가 없음을 린터에게 증명해야 한다. 예를 들어 serverUrl 컴포넌트 밖으로 이동시킴으로써 반응형이 아니며 리렌더링시에도 변경되지 않음을 증명할 수 있다.

const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore

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

이제 serverUrl은 반응형 값이 아니므로(그리고 다시 렌더링할 때 변경할 수 없으므로) 의존성이 될 필요가 없다. Effect의 코드가 반응형 값을 사용하지 않는다면 의존성 목록은 비어 있어야 한다([]).

const serverUrl = 'https://localhost:1234'; // Not a reactive value anymore
const roomId = 'music'; // Not a reactive value anymore

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

빈 의존성이 있는 Effect는 컴포넌트의 props나  state가 변경되어도 다시 실행되지 않는다. 

 

* 기존 코드베이스가 있는 경우 이와 같이 린터를 억제하는 Effect가 있을 수 있다.

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

의존성이 코드와 일치하지 않으면 버그가 발생할 위험이 높다. 린터를 억제하는 것은 곧 Effect가 의존하는 값에 대해 리액트에 "거짓말"을 하는 것으로 의존성들이 불필요하다는 것을 증명해야 한다. 

 

Effect의 이전 state를 기반으로 state 업데이트하기

Effect의 이전 state를 기반으로 state를 업데이트하려는 경우 문제가 발생할 수 있다. 

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // You want to increment the counter every second...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... but specifying `count` as a dependency always resets the interval.
  // ...
}

 

count는 반응형 값이므로 의존성 목록에 지정되어야 한다. 다만 이로 인해 count가 변경될 때마다 Effect를 다시 클린업하고 셋업해줘야 한다. 이는 이상적이지 않다.

이 문제를 해결하려면 setCount에 c => c + 1 state 업데이터를 전달해야 한다.

import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ Pass a state updater
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅ Now count is not a dependency

  return <h1>{count}</h1>;
}

이제 count + 1 대신 c => c + 1을 전달하므로 Effect는 더 이상 count에 의존할 필요가 없다. 이 수정으로 count가 변경될 때마다 interval을 다시 클린업하고 셋업할 필요가 없다. 

불필요한 객체 의존성 제거하기 

Effect가 렌더링 중에 생성된 객체 또는 함수에 의존하는 경우 필요 이상으로 자주 실행될 수 있다. 예를 들어 options 객체는 각 렌더링마다 다른 값이므로, 이 Effect는 매 렌더링 시에 다시 연결하게 된다.

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

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

  const options = { // 🚩 This object is created from scratch on every re-render
    serverUrl: serverUrl,
    roomId: roomId
  };

  useEffect(() => {
    const connection = createConnection(options); // It's used inside the Effect
    connection.connect();
    return () => connection.disconnect();
  }, [options]); // 🚩 As a result, these dependencies are always different on a re-render
  // ...

렌더링 중에 생성된 객체를 의존성으로 사용하면 안된다. 대신 Effect 내에서 객체를 생성해야 한다. 

// App.js

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

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

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

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

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  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>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

// chat.js

export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

이제 Effect 내부에 options 객체를 만들었으므로 Effect는 오직 roomId 문자열에만 의존하게 된다. 

 

이 수정으로 input에 타이핑해도 채팅이 다시 연결되지 않는다. 다시 만들어지는 객체와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않는다. 

불필요한 함수 의존성 제거하기

Effect가 렌더링 중에 생성된 객체 또는 함수에 의존하는 경우 필요 이상으로 자주 실행될 수 있다. 예를 들어, createOptions함수가 렌더링할 때마다 다르기 때문에 이 Effect는 렌더링할때마다 다시 연결된다. 

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

  function createOptions() { // 🚩 This function is created from scratch on every re-render
    return {
      serverUrl: serverUrl,
      roomId: roomId
    };
  }

  useEffect(() => {
    const options = createOptions(); // It's used inside the Effect
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, [createOptions]); // 🚩 As a result, these dependencies are always different on a re-render
  // ...

렌더링할 때마다 함수를 처음부터 새로 만드는 것 자체는 문제가 되지 않는다. 최적화할 필요도 없다. 그러나 이 함수를 Effect의 의존성으로 사용하면 Effect가 다시 렌더링할 때마다 다시 실행된다. 

 

렌더링 중에 생성된 함수를 의존성으로 사용하면 안된다. 대신 Effect 내에서 선언한다. 

// App.js

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

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

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

  useEffect(() => {
    function createOptions() {
      return {
        serverUrl: serverUrl,
        roomId: roomId
      };
    }

    const options = createOptions();
    const connection = createConnection(options);
    connection.connect();
    return () => connection.disconnect();
  }, [roomId]);

  return (
    <>
      <h1>Welcome to the {roomId} room!</h1>
      <input value={message} onChange={e => setMessage(e.target.value)} />
    </>
  );
}

export default function App() {
  const [roomId, setRoomId] = useState('general');
  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>
      <hr />
      <ChatRoom roomId={roomId} />
    </>
  );
}

// chat.js

export function createConnection({ serverUrl, roomId }) {
  // A real implementation would actually connect to the server
  return {
    connect() {
      console.log('✅ Connecting to "' + roomId + '" room at ' + serverUrl + '...');
    },
    disconnect() {
      console.log('❌ Disconnected from "' + roomId + '" room at ' + serverUrl);
    }
  };
}

 

이제 Effect내에서 ceateOptions 함수를 정의하면 Effect 자체는 roomId 문자열에만 의존한다. 이 수정으로 input에 타이핑해도 채팅이 다시 연결되지 않는다. 새로 생성되는 함수와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않는다.

Effect에서 최신 props 및 state 읽기

기본적으로 Effect에서 반응형 값을 읽을 때엔 이를 의존성으로 추가해야 한다. 이렇게 하면 Effect가 해당 값의 모든 변경에 "반응"하도록 할 수 있다. 대부분의 의존성에서 원하는 동작이다.

 

그러나 때로는 Effect에 "반응"하지 않고도 Effect에서 최신 props와 state를 읽고 싶을 때가 있다. 예를 들어, 페이지 방문 시마다 장바구니에 있는 품목의 수를 기록한다고 가정해보자. 

function Page({ url, shoppingCart }) {
  useEffect(() => {
    logVisit(url, shoppingCart.length);
  }, [url, shoppingCart]); // ✅ All dependencies declared
  // ...
}

 

url이 변경될 때마 새 페이지 방문을 기록하되 shoppingCart만 변경되는 경우는 기록하지 않으려면 어떻게 해야하는지?

반응성 규칙을 위반하지 않으면서 shoppingCart를 의존성에서 제외할 수는 없다. 그러나 코드가 Effect내부에서 호출되더라도 변경 사항에 "반응"하지 않도록 표현할 수 있다. useEffcetEvent 훅을 사용하여 Effect Event를 선언하고 shoppingCart를 읽는 코드를 그 안으로 이동시킨다.

 

function Page({ url, shoppingCart }) {
  const onVisit = useEffectEvent(visitedUrl => {
    logVisit(visitedUrl, shoppingCart.length)
  });

  useEffect(() => {
    onVisit(url);
  }, [url]); // ✅ All dependencies declared
  // ...
}

Effect Events는 반응형이 아니므로 항상 Effect의 의존성에서 제외해야 한다. 이를 통해 반응형이 아닌 코드(일부 props 및 state의 최신 값을 읽을 수 있는 코드) 그 안에 넣을 수 있다. 예를 들어 onVisit내부에서 shoppingCart를 읽으면 shoppingCart가 Effect를 다시 실행하지 않도록 할 수 있다. 향후에는 린터가 useEffectEvent를 지원하여 의존성에서 Effect Event를 생략하는지 확인한다.

 

서버와 클라이언트에 서로 다른 콘텐츠 표시하기 

서버 렌더링(직접이든 프레임워크를 통해서든)을 사용하는 앱의 경우, 컴포넌트는 두 가지 다른 환경에서 렌더링된다. 서버에서는 초기 HTML을 생성하기 위해 렌더링된다. 클라이언트에서 React는 렌더링 코드를 다시 실행하여 이벤트 핸들러를 해당 HTML에 첨부할 수 있도록 한다. 그렇기 때문에 hydration이 작동하려면 클라이언트와 서버의 첫 렌더링 결과가 동일해야 한다.

 

드물지만 클라이언트에 다른 콘텐츠를 표시해야 하는 경우가 있을 수 있다. 예를 들어 앱이 localStorage에서 일부 데이터를 읽는 경우 서버에서는 이를 수행할 수 없다. 일반적으로 이를 구현하는 방법은 다음과 같다.

function MyComponent() {
  const [didMount, setDidMount] = useState(false);

  useEffect(() => {
    setDidMount(true);
  }, []);

  if (didMount) {
    // ... return client-only JSX ...
  }  else {
    // ... return initial JSX ...
  }
}

앱이 로드되는 동안 사용자는 초기 렌더링 결과물을 본다. 그런 다음 앱이 로드 및 hydrated되면 이제 Effect가 실행되면서 didMount를 true로 설정하여 리렌더링을 촉발한다. 이로 인해 클라이언트 전용 렌더링 결과물로 전환된다. Effect는 서버에서 실행되지 않기 때문에 서버 렌더링 중에는 didMount는 fals이다.

 

이 패턴은 되도록 사용하지 않는 것이 좋다. 연결 속도가 느린 사용자가 초기 콘텐츠를 꽤 오랜 시간(수 초)동안 보게 되므로 컴포넌트의 모양이 갑작스럽게 변경되는걸 원하지 않을 것이라는 점에 유의해야 한다. 대부분의 경우 CSS를 사용하여 조건부로 다른 것을 표시함으로써 이러한 필요성을 피할 수 있다.

 

'React-study > dil' 카테고리의 다른 글

[DIL] useMemo  (0) 2024.06.17
[DIL] useLayoutEffect  (0) 2024.06.11
[DIL] useRef  (0) 2024.05.30
[DIL] useContext  (0) 2024.05.29
[DIL] useReducer(2)  (0) 2024.05.28