본문 바로가기

리액트 공식 문서 읽기

25화: Synchronizing with Effects

이 글은 단순히 리액트 공식 문서를 읽고 베꼈을 뿐이다!! 학습 목적이라면 공식 문서를 보는 걸 추천한다.

 

참새. Unsplash 에 Christian Søgaard 님이 올림

공식 문서

https://react.dev/learn/synchronizing-with-effects

 

Synchronizing with Effects – React

The library for web and native user interfaces

react.dev

이펙트와 이벤트

  • 렌더링 코드
    • 컴포넌트 최상단에 있음
    • props, 상태를 받아서 JSX를 반환
    • 순수해야 함
    • 단순히 계산해서 결과를 반환하기만 함
  • 이벤트 핸들러
    • 단순 계산만 하는게 아님
    • side effect 포함
  • 이펙트는 특정 이벤트로부터 생기는 게 아닌 렌더링 자체에서 나타나는 side effect를 명시할 수 있게 해줌
  • 이펙트는 화면 업데이트 후 commit 단계의 맨 마지막에 발생
  • 리액트 컴포넌트가 외부 시스템과 연결하도록 도와줌

이펙트가 필요 없을지도?

  • 이펙트는 리액트 코드에서 벗어나 외부 시스템과 연동하려고 쓴다는 걸 기억하기
  • 만약 이펙트가 하나의 상태에 기반해서 다른 상태들을 바꾸는 것에 불과하다면 이펙트가 필요 없을지도 모름

이펙트 쓰기 3단계

1단계: 이펙트 정의하기

  • import { useEffect } from 'react';
  • 컴포넌트의 최상단(top level)에서 useEffect 사용하기
  • useEffect는 render가 화면에 반영이 끝날때까지 기다렸다가 해당 이펙트 코드를 실행함
  • side effect를 일으키는 코드를 useEffect 안에 넣기
  • 기본적으로 이펙트는 매 렌더링 이후에 무조건 일어남: 이펙트 안에서 상태를 변경하는 경우 무한루프 유발 가능
  • 만약 이펙트가 하나의 상태에 기반해서 다른 상태들을 바꾸는 것에 불과하다면 이펙트가 필요 없을지도 모름

2단계: 이펙트 의존성 명시하기

  • 기본적으로 이펙트는 매 렌더링 이후에 무조건 일어남
  • 항상 이펙트를 실행하면 느릴 수 있음
  • 항상 이펙트를 실행하는 것 자체를 원하지 않을 수 있음
  • useEffect에서는 의존성을 적어서 불필요한 이펙트의 실행을 건너뛸 수 있음
  • 의존성 배열에 적혀있는 값이 이전 렌더링과 같다면 해당 이펙트를 실행하지 않음
  • 의존성 '배열' 이니까 여러 값이 있을 수 있음: 배열 안의 모든 값이 이전 렌더링과 같아야 이펙트를 건너뜀
  • 각 의존성 값은 Object.is 연산으로 같은지 비교함
  • 의존성은 내가 정하는 게 아님: 제대로 안 하면 lint error를 받게 될 것
  • 의존성 배열이 아예 없으면 매 렌더링마다, 빈 배열이면 컴포넌트 mount 시에만, 의존성이 들어 있으면 mount와 해당 의존성 값이 바뀔 때 이펙트를 실행
  • ref는 의존성 배열에 없어도 됨: 항상 같은 객체를 가리키니까 불변성이 보장됨

3단계: 필요하다면 cleanup 함수 사용하기

  • 이펙트에서 cleanup 함수를 return 하면 이펙트의 재실행 직전 및 컴포넌트 unmount 전에 이전에 실행한 이펙트의 cleanup 함수를 실행함
  • 리액트는 Strict Mode에서 컴포넌트를 두 번 mount하므로 cleanup이 필요한지 아닌지 쉽게 판단 가능
  • 핵심은 이펙트가 한 번 실행되는 것과 실행 -> cleanup -> 실행 사이클이 완전히 동일하게 보이도록 만드는 것

개발 단계에서 이펙트가 두 번 실행되지 않게 하는 법은 없나요?

  • 질문이 잘못됨. "두 번 mount 되더라도 잘 작동하도록 이펙트를 고치는 법은 뭐죠?" 가 맞음
  • 대부분의 경우 cleanup 함수를 만들면 해결
  • 핵심은 이펙트가 한 번 실행되는 것과 실행 -> cleanup -> 실행 사이클이 완전히 동일하게 보이도록 만드는 것
  • 많은 이펙트는 아래의 패턴들 중 하나랑 유사할 것이니 참고
  1. 리액트가 아닌 위젯 조절
    • <dialog>태그를 mount 시에 열기만 하는 경우: 두 번 열면 터짐 -> cleanup으로 닫아줘야 함
    • 지도의 확대 정도를 상태값과 동기화하는 경우: 두 번 실행하면 살짝 느릴 순 있지만 터지진 않음 -> cleanup 불필요
  2. 이벤트 구독
    • addEventListener: cleanup으로 removeEventListener도 같이 해야 함
  3. 애니메이션
    • cleanup 함수로 애니메이션 원위치(초기화) 필요
  4. fetch
    • cleanup에서 AbortController로 fetch를 멈추거나 fetch 결과를 무시할 수 있도록 처리해야 함
    • 개발 단계에서는 fetch를 두 번 할 수 있다는 걸 잊지 말기
    • 그게 싫으면 같은 요청에 대해 결과를 캐싱하는 기능을 만들 것
  5. 분석 결과 보내기
    • 로깅 같은 애들은 개발 모드에서 두 번씩 실행되는 게 싫을 수 있음
    • 근데 그대로 냅두는 걸 추천함
    • cleanup을 쓰지 않더라도 이용자는 차이를 모름
    • 단순한 로깅은 두 번 실행되도 어플리케이션 동작에 아무 영향 없는 게 정상임
  6. 이펙트가 아니야: 어플리케이션 초기화
    • 어플리케이션이 시작할 때 딱 한 번 실행해야 하는 로직 (예: msw 켜기)
    • 얘네는 컴포넌트 밖에 배치하기
  7. 이펙트가 아니야: 상품 구매
    • 이용자의 행동에 반응해야하는 로직은 이벤트 핸들러에 넣기

이펙트에서 fetch하는 방법들은 뭐가 있나요?

  • 이펙트 안에서 fetch하는 건 유명한 방법이긴 함
  • 근데 이건 수동적인 방법이고 단점들이 좀 있음
    • 이펙트는 서버에서 안 돌아감: 서버 사이드 렌더링을 하는 경우 데이터 없이 html을 줄 가능성이 농후
    • 이펙트에서 직접 데이터를 받아오는 건 연쇄작용을 불러올 수 있음: 부모 컴포넌트가 렌덜이되면 본인 데이터도 받아오고 자식도 리렌더링되면서 자식도 다시 본인 데이터를 받아와서 느릴 수 있음
    • 이펙트에서 직접 데이터를 받아온다는 건 선 로딩이나 데이터 캐싱을 안 한다는 뜻임: unmount하고 다시 mount하면 또 fetch 해야함
    • 코드가 못생겨짐: 이펙트 안에서의 fetch로 일어날 수 있는 버그(예: race condition)들을 해결하려면 보일러플레이트 코드를 많이 써야 할 수도 있음
  • 프레임워크를 쓴다면 자체 제공하는 fetch 방법들 쓰기
  • React Query, useSWR, React Router 6.4+ 처럼 클라이언트 사이드 캐싱을 제공하는 라이브러리를 쓰거나 만들기

반성문

이펙트를 너무 무지성으로 써왔다. 비상탈출구임을 항상 생각하자!!