이 글은 단순히 리액트 공식 문서를 읽고 베꼈을 뿐이다!! 학습 목적이라면 공식 문서를 보는 걸 추천한다.
공식 문서
https://react.dev/learn/synchronizing-with-effects
이펙트와 이벤트
- 렌더링 코드
- 컴포넌트 최상단에 있음
- 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 -> 실행 사이클이 완전히 동일하게 보이도록 만드는 것
- 많은 이펙트는 아래의 패턴들 중 하나랑 유사할 것이니 참고
- 리액트가 아닌 위젯 조절
<dialog>
태그를 mount 시에 열기만 하는 경우: 두 번 열면 터짐 -> cleanup으로 닫아줘야 함- 지도의 확대 정도를 상태값과 동기화하는 경우: 두 번 실행하면 살짝 느릴 순 있지만 터지진 않음 -> cleanup 불필요
- 이벤트 구독
addEventListener
: cleanup으로removeEventListener
도 같이 해야 함
- 애니메이션
- cleanup 함수로 애니메이션 원위치(초기화) 필요
- fetch
- cleanup에서
AbortController
로 fetch를 멈추거나 fetch 결과를 무시할 수 있도록 처리해야 함 - 개발 단계에서는 fetch를 두 번 할 수 있다는 걸 잊지 말기
- 그게 싫으면 같은 요청에 대해 결과를 캐싱하는 기능을 만들 것
- cleanup에서
- 분석 결과 보내기
- 로깅 같은 애들은 개발 모드에서 두 번씩 실행되는 게 싫을 수 있음
- 근데 그대로 냅두는 걸 추천함
- cleanup을 쓰지 않더라도 이용자는 차이를 모름
- 단순한 로깅은 두 번 실행되도 어플리케이션 동작에 아무 영향 없는 게 정상임
- 이펙트가 아니야: 어플리케이션 초기화
- 어플리케이션이 시작할 때 딱 한 번 실행해야 하는 로직 (예: msw 켜기)
- 얘네는 컴포넌트 밖에 배치하기
- 이펙트가 아니야: 상품 구매
- 이용자의 행동에 반응해야하는 로직은 이벤트 핸들러에 넣기
이펙트에서 fetch하는 방법들은 뭐가 있나요?
- 이펙트 안에서 fetch하는 건 유명한 방법이긴 함
- 근데 이건 수동적인 방법이고 단점들이 좀 있음
- 이펙트는 서버에서 안 돌아감: 서버 사이드 렌더링을 하는 경우 데이터 없이 html을 줄 가능성이 농후
- 이펙트에서 직접 데이터를 받아오는 건 연쇄작용을 불러올 수 있음: 부모 컴포넌트가 렌덜이되면 본인 데이터도 받아오고 자식도 리렌더링되면서 자식도 다시 본인 데이터를 받아와서 느릴 수 있음
- 이펙트에서 직접 데이터를 받아온다는 건 선 로딩이나 데이터 캐싱을 안 한다는 뜻임: unmount하고 다시 mount하면 또 fetch 해야함
- 코드가 못생겨짐: 이펙트 안에서의 fetch로 일어날 수 있는 버그(예: race condition)들을 해결하려면 보일러플레이트 코드를 많이 써야 할 수도 있음
- 프레임워크를 쓴다면 자체 제공하는 fetch 방법들 쓰기
- React Query, useSWR, React Router 6.4+ 처럼 클라이언트 사이드 캐싱을 제공하는 라이브러리를 쓰거나 만들기
반성문
이펙트를 너무 무지성으로 써왔다. 비상탈출구임을 항상 생각하자!!
'리액트 공식 문서 읽기' 카테고리의 다른 글
27화: Lifecycle of Reactive Effects (0) | 2023.06.22 |
---|---|
26화: You Might Not Need an Effect (0) | 2023.06.22 |
24화: Manipulating the DOM with Refs (0) | 2023.06.19 |
23화: Referencing Values with Refs (0) | 2023.06.19 |
22화: Scaling Up with Reducer and Context (0) | 2023.06.18 |