useEffect

moreas·2024년 1월 25일

react

목록 보기
2/3
post-thumbnail

❄️ useEffect 란?

useEffect는 외부 시스템과 컴포넌트를 동기화하는 React Hook


외부 시스템과 컴포넌트를 동기화 ?

  • 일부 컴포넌트에서는 외부 시스템과 동기화를 해야 할 경우가 있는데, state을 기준으로 React와 상관없는 구성 요소 제어, 서버 연결 설정, 구성 요소가 화면에 나타날 때 분석 목적의 로그를 전송할 수도 있다. Effect를 사용하면 렌더링 후 코드를 실행하여 React 외부 시스템과 컴포넌트를 동기화할 수 있다.

Effect와 이벤트의 다른 점

  • 컴포넌트 내부는 2가지 로직 유형으로 나눌 수 있는데,
    ① 렌더링 코드 (props와 state를 적절히 변형해 결과적으로 JSX를 반환하는 순수함을 요구하는 코드)
    ② 이벤트 핸들러 (단순한 계산 용도가 아닌 무언가를 하는 컴포넌트 내부의 중첩 함수. 사용자의 특정 작업으로 인해 발생하는 부수효과도 포함한다.)

  • 우리가 알고 있는 Effect는 부수효과를 특정한다. 부수효과는 렌더링 과정 중에 발생하는 것이고, 이벤트는 사용자의 직접적인 특정 인터페이스 동작으로 발생하는 것에서 차이가 있다. 서버 연결, 외부 시스템과 동기화 등은 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]);
  // ...
}

1. 선언

import { useEffect } from 'react';
// 최상단에 이펙트훅을 임포트해준다.

function MyComponent() {
  useEffect(() => {
    // 이곳의 코드는 *모든* 렌더링 후에 실행됨
  });
  return <div />;
}

2. 의존성 배열 지정

  • React에게 Effect를 불필요하게 재실행하지 않도록 하려면 useEffect 두 번째 인자로 의존성(dependencies) 배열을 지정한다.

  • 의존성 배열에는 여러 개의 종속성을 포함할 수 있다.

  • 의존성을 선택할 수 없다. 의존성 배열에 지정한 종속성이 Effect 내부의 코드를 기반으로 React가 기대하는 것과 일치하지 않으면 린트 에러가 발생하게 됨 (이건 처음 알게 되서 신기했다.)

useEffect(() => {
  // 모든 렌더링 후에 실행됩니다
});

useEffect(() => {
  // 마운트될 때만 실행됩니다 (컴포넌트가 나타날 때)
}, []);

useEffect(() => {
 // 마운트될 때 실행되며, *또한* 렌더링 이후에 a 또는 b 중 하나라도 변경된 경우에도 실행됩니다
}, [a, b]);

3. 클린업 추가하기

  • 클린업은 필수가 아니므로 필요한 경우에 클린업 함수를 추가할 수 있다.
  • 이 함수는 useEffect에서 설정된 side effect를 청소하는 데 사용되는데, 주요한 이유는 다음과 같이 정리해볼 수 있다.
  1. 메모리 누수 방지

cleanup 함수는 컴포넌트가 언마운트(unmount)될 때 호출된다. 이 과정은 컴포넌트가 사라질 때 컴포넌트가 생성했던 side effect를 정리하는 데 매우 중요한데, 예를 들어 컴포넌트가 WebSocket 연결을 설정하거나 setInterval을 사용하여 반복 작업을 설정한 경우, 컴포넌트가 언마운트되면 이들 작업을 정리하지 않으면 메모리 누수가 발생할 수 있다.

  1. 올바른 상태 유지

cleanup 함수는 새로운 side effect를 실행하기 전에도 호출된다. 이는 useEffect의 의존성 배열(dependency array)에 있는 값이 변경될 때마다 발생합니다. 이로 인해 이전에 설정된 side effect를 청소하고, 새로운 side effect를 실행하여 최신의 상태를 반영할 수 있습니다.

  1. 이벤트 핸들러 제거

컴포넌트가 언마운트되었을 때 이벤트 리스너를 제거하지 않으면 이벤트 리스너가 콜백 함수를 계속 호출하려고 시도하면서 문제가 발생할 수 있기 때문에 제거해주는 것이 좋다.

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

export default function ChatRoom() {
  useEffect(() => {
    const connection = createConnection();
    connection.connect();
    return () => connection.disconnect();
  }, []);
  return <h1>채팅에 오신걸 환영합니다!</h1>;
}

🏔️ reference

외부 시스템과 연결

  • React에 제어되지 않는 네트워크나 브라우저 API, 또는 서드파티 라이브러리와의 연결 등을 외부 시스템(external)으로 부르고 이를 유지하기 위해 이펙트 훅을 사용하게 된다.

  • 공식 문서에서 설명하고 있는 예시로는 ① 채팅 서버와 연결, ②전역 브라우저 이벤트 감시, ③ 애니메이션 동작 ④ 모달 제어하기 ⑤ 요소의 가시성 (브라우저 DOM 조작 시 뷰포트 내에서 보이는 부분 확인하기) 이 있다.

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

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

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]);

주목할 점

  • Effect 내부에서 fetch나 axios 호출을 작성하여 데이터를 페칭하는 것은 클라이언트 사이드 앱에서 가장 보편적으로 쓰이는 방법이다. 하지만 이것은 매우 수동적인 접근 방식이며 큰 단점이 있다고 한다.
  1. Effect는 서버에서는 실행되지 않는다. 이는 초기 서버 렌더링 된 HTML이 데이터가 없는 state를 먼저 포함한 후 렌더링한 다음 데이터를 받아오기 위해 또 로드하기 때문에 효율적이지는 않다.

  2. Effect 내부에서 직접 페칭을 하는 것은 네트워크 폭포(network waterfalls)가 생성되기 쉽다. 부모 컴포넌트 렌더링 후 일부 데이터를 페칭하고 나서 자식 컴포넌트가 렌더링 되고 데이터를 페칭하기 시작하기 때문에 모든 데이터를 병렬로 가져오는 것보다 훨씬 느린 방법이다.

  3. Effect 내부에서 직접 데이터를 페칭하는 것은 일반적으로 데이터를 미리 로드하거나 캐싱하지 않기 때문에 컴포넌트가 다시 마운트되었을 때 데이터를 다시 가져와야 한다. 사용하기 불편하다.

++ 4. 이건 내 개인적인 의견이지만 유즈이펙트 안에서 페칭 관련 코드를 작성할 경우 코드양이 늘어난다.(게다가 매번 데이터를 불러올 때마다 같은 코드를 작성해야 함) 효율성 측면에서 이 부분은 로직을 따로 빼서 임포트하는 식으로 작성 하는 것이 훨씬 효율적이라고 생각한다.

==> 보통 팀 프로젝트에서 유즈이펙트에 직접 데이터를 페칭해서 사용하는 식으로 로직을 많이 짰었는데 직접 데이터 페칭을 권장하지 않는단 걸 보니 꽤나 충격..! 이었다. 곧 리액트 토이플젝을 하나 만들 것 같은데 거기서는 내장된 데이터 페칭 기능을 활용하거나 따로 훅을 만들어서 적용해봐야겠다.

주의할 점

  • 기본적으로 Effect는 모든 렌더링 후에 실행된다.

  • React에서는 렌더링이 DOM 수정과 같은 부수 효과를 포함해서는 안된다.

  • Effect는 client 환경에서만 동작한다. 서버 렌더링에서는 동작하지 않는다.


☃️ trouble shooting

Effect가 매 리렌더링마다 실행될 때

  • 먼저 의존성 배열에 값을 추가했는지 확인
  • 의존성 배열을 명시했음에도 Effect가 여전히 반복해서 실행된다면 의존성이 렌더링마다 다르기 때문
  • 콘솔에서 수동으로 의존성을 기록하는 방법도 있다.

Effect가 무한 반복될 때

Effect가 무한 반복되려면 다음 두 가지 조건이 충족되어야 한다.

  • Effect에서 state를 업데이트함.
  • 변경된 state가 리렌더링을 유발하며, 이에 따라 Effect의 종속성이 변경됨.

이 경우에는 useEffect훅의 쓰임새를 확인해야 한다. 외부 시스템이 없다면 Effect를 제거해서 로직을 단순화할 수 있는지 고민해볼 수 있다.

Effect가 시각적인 작업을 수행하며, 실행되기 전에 깜빡임 있는 경우

  • Effect가 브라우저가 화면을 그리는 것을 차단해야 하는 경우 useEffect를 useLayoutEffect로 대체할 수 있다. 대부분은 Effect에는 필요하지 않은 방법이지만 브라우저 페인팅 이전에 Effect를 실행하는 것이 중요한 경우(사용자가 보기 전에 툴팁의 위치를 측정하고 지정해야 하는 경우)에는 사용할 수 있다.


⛷️ useEffect가 불필요한 순간

  • 한 마디로 useEffect는 외부 시스템과 컴포넌트 사이에 동기화가 필요없는 경우(부 props 또는 state가 변경될 때 컴포넌트의 state를 업데이트하려는 경우)에는 필요하지 않다.

렌더링을 위해 데이터를 변환하는 데 Effect가 필요하지 않음

리스트가 변경될 때 state 변수를 업데이트하는 Effect를 작성하는 대신, 컴포넌트의 최상위 레벨에서 모든 데이터를 변환하고 props나 state가 변경될 때마다 해당 코드가 자동으로 다시 실행되도록 만들 수 있다.

사용자 이벤트를 핸들링하는 데 Effect가 필요하지 않음

구매 버튼 클릭 이벤트 핸들러에서는 정확히 어떤 일이 일어났는지 알 수 있지만 useEffect가 실행될 때까지 사용자가 무엇을 했는지 (예: 어떤 버튼을 클릭 했는지) 알 수 없다. 그렇기 때문에 일반적으로 해당되는 이벤트 핸들러에서 사용자 이벤트를 핸들링하는 것이 더 적절하다.

profile
Everything is connected 🐶 좀 더 나은 개발을 위해

0개의 댓글