< 목차 >
0. TL;DR
1. ref로 노드 가져오기
2. 직접 만든 컴포넌트의 DOM 노드에 접근하기
3. React가 ref를 부여할 때
Effect로 동기화하기 (Synchronizing with Effects)
Effect를 사용하면 렌더링 후, 특정 코드를 실행하여 React 외부의 시스템과 컴포넌트를 동기화할 수 있다.
0. TL;DR
- Effect는 특정 상호작용이 아닌 렌더링 자체에 의해 발생
- 코드가 다시 실행되길 원하지 않는 경우, Effect 내부 코드를 수정하여 의존성 배열에 해당 종속성이 필요하지 않도록 만들자
- Effect를 사용하면 컴포넌트를 외부 시스템(타사 API, 네트워크 등)과 동기화 가능
- Effect는 모든 렌더링(초기 렌더링 포함) 후에 실행됨
- Effect가 다시 마운트로 인해 중단된 경우 클린업 함수를 구현해야 함
1. Effect란 무엇이고, 이벤트와는 어떻게 다른가
Effect에 대해 자세히 알아보기 전에, 컴포넌트 내부의 2가지 로직 유형에 대해 알아야 한다.
- 렌더링 코드를 주관하는 로직
: 컴포넌트의 최상단에 위치하며, props와 state를 적절히 변형해 결과적으로 JSX를 반환한다. 렌더링 코드 로직은 순수해야 한다. 수학 공식처럼 결과만 계산해야 하고, 그 외에는 아무것도 하지 말아야 한다. - 이벤트 핸들러
: 단순한 계산 용도가 아닌 무언가를 하는 컴포넌트 내부의 중첩 함수다. 이벤트 핸들러는 입력 필드를 업데이트하거나, 제품을 구입하기 위해 HTTP POST 요청을 보내거나, 사용자를 다른 화면으로 이동시킬 수 있다. 이벤트 핸들러에는 특정 사용자 작업(예: 버튼 클릭 또는 입력)으로 인해 발생하는 “부수 효과”(이러한 부수 효과가 프로그램 상태를 변경)를 포함한다.
가끔은 이것으로 충분하지 않기도 한다.
Effect는 렌더링 자체에 의해 발생하는 부수 효과를 특정하는 것으로, 특정 이벤트가 아닌 렌더링에 의해 직접 발생한다.
예를 들어, 채팅에서 메시지를 보내는 것은 이벤트다.
왜냐하면 이것은 사용자가 특정 버튼을 클릭함에 따라 직접적으로 발생한다. 그러나 서버 연결 설정은 Effect다. 컴포넌트의 표시를 주관하는 어떤 상호작용과도 상관없이 발생해야 한다. Effect는 커밋이 끝난 후, 화면 업데이트가 이루어지고 나서 실행된다.
💡 ‘Effect’는 렌더링에 의한 side effect를 의미
2. Effect가 필요 없을지도 모른다
컴포넌트에 Effect를 무작정 추가하지 말자.
Effect는 주로 React 코드를 벗어난 특정 외부 시스템과 동기화하기 위해 사용된다. (브라우저 API, 써드파티 위젯, 네트워크 등을 포함)
만약, Effect가 단순히 다른 상태에 기반하여 일부 상태를 조정하는 경우엔 필요하지 않을 수 있다.
3. Effect를 작성하는 법
- Effect 선언: 기본적으로 Effect는 모든 commit 이후에 실행된다.
- Effect 의존성 지정: 대부분의 Effect는 모든 렌더링 후가 아닌 필요할 때만 다시 실행되어야 한다. 예를 들어, 페이드 인 애니메이션은 컴포넌트가 나타날 때에만 트리거 되어야 한다. 채팅방에 연결/연결해제 하는 것은 컴포넌트가 나타나거나 사라질 때 또는 채팅방이 변경될 때만 발생해야 한다. 의존성을 지정하여 이를 제어하자.
- 필요한 경우, 클린업 함수 추가: 일부 Effect는 수행 중이던 작업을 중지, 취소 또는 정리하는 방법을 지정해야 할 수 있다. 예를 들어, 연결은 연결 해제가 필요하며, 구독은 구독 취소가 필요하고, 불러오기(fetch)는 취소 또는 무시가 필요하다. 이런 경우에 Effect에서 *클린업 함수(cleanup function)*를 반환하자.
Effect 선언하고 사용하는 법
다음은 React 상태와 동기화된 외부 시스템인 브라우저 미디어 API 예제다.
// 1. 일반적인 사용법
// (1) useEffect Hook을 import
import { useEffect } from 'react';
// (2) 컴포넌트의 최상위 레벨에서 호출하고 Effect 내부에 코드 넣기
function MyComponent() {
useEffect(() => {
// 이곳의 코드는 *모든* 렌더링 후에 실행됨
});
return <div />;
}
// 컴포넌트가 렌더링 될 때마다 React는 화면을 업데이트한 다음, useEffect 내부의 코드를 실행함
// useEffect는 화면에 렌더링이 반영될 때까지 코드 실행을 ‘지연’시킴
// 2. 외부 시스템과 동기화하기 위한 Effect 사용법
// (1) isPlaying이라는 props를 통해 재생 중인지 일시 정지 상태인지 제어
<VideoPlayer isPlaying={isPlaying} />;
// (2) 커스텀 VideoPlayer 컴포넌트는 내장 브라우저 <video> 태그를 렌더링
function VideoPlayer({ src, isPlaying }) {
// TODO: isPlaying을 활용하여 무언가 수행하기
return <video src={src} />;
}
// 그러나 <video> 태그에는 isPlaying prop이 없음
// 이를 제어하는 유일한 방법은 DOM 요소에서 수동으로 play() 및 pause() 메서드를 호출하는 것
// isPlaying prop의 값(현재 비디오가 재생 중인지 여부)을 play() 및 pause()와 같은 호출과 동기화
// 아래처럼 구현하면 에러 뜸
// 렌더링 중에 DOM 노드를 조작하려고 시도하기 때문
if (isPlaying) {
ref.current.play(); // 렌더링 중에 이를 호출하는 것이 허용되지 않음
} else {
ref.current.pause(); // 역시 이렇게 호출하면 바로 위의 호출과 충돌이 발생
}
// 해결책은 부수 효과를 렌더링 연산에서 분리하기 위해 useEffect로 감싸는 것
import { useEffect, useRef } from 'react';
function VideoPlayer({ src, isPlaying }) {
const ref = useRef(null);
// DOM 업데이트를 Effect로 감싸면 React가 화면을 업데이트한 다음에 Effect가 실행됨
useEffect(() => {
if (isPlaying) {
ref.current.play();
} else {
ref.current.pause();
}
});
return <video ref={ref} src={src} loop playsInline />;
}
// VideoPlayer 컴포넌트가 렌더링될 때(처음 호출하거나 다시 렌더링 할 때) 다음과 같은 일이 발생함
// (1) React는 화면을 업데이트하여 <video> 태그가 올바른 속성과 함께 DOM에 있는지 확인
// (2) React는 Effect를 실행
// (3) 마지막으로, Effect에서는 isPlaying 값에 따라 play() 또는 pause()를 호출
필요하다면 클린업 함수를 추가하자
// 사용자에게 표시될 때 채팅 서버에 연결해야 하는 ChatRoom 컴포넌트를 작성하는 예제
import { useEffect } from 'react';
import { createConnection } from './chat.js';
export default function ChatRoom() {
useEffect(() => {
// createConnection() API가 주어지며,
// 이 API는 connect() 및 disconnect() 메서드를 가진 객체를 반환
const connection = createConnection();
connection.connect();
// [클린업 함수] 컴포넌트가 언마운트(제거)될 때에도 마지막으로 호출!
return () => {
connection.disconnect();
};
// 매번 리렌더링 후에 채팅 서버에 연결하는 것은 느리므로 의존성 배열 추가
// Effect 내부의 코드는 어떠한 props나 상태도 사용하지 않으므로, 의존성 배열은 [](빈 배열)
}, []);
return <h1>채팅에 오신걸 환영합니다!</h1>;
}
4. 어떻게 Effect가 다시 마운트된 후에도 작동하도록 만들까?
정답은 클린업 함수를 구현하는 것!
클린업 함수는 Effect가 수행하던 작업을 중단하거나 되돌리는 역할을 한다.
클린업을 잘 구현하면 Effect를 한 번 실행하는 것(Strict Mode를 해제한 것처럼)과 실행, 클린업, 이후 다시 실행하는 것 사이에 사용자가 느끼는 차이가 없다. (= 불편함이 없음)
이펙트가 두 번 실행되는 것을 막기 위해 ref를 사용하지 말자
개발 과정에서 Effect가 2번 실행되는 것을 막기 위한 일반적인 함정은 Effect가 2번 이상 실행되는 것을 막기 위해 ref를 사용하는 것이다.
const connectionRef = useRef(null);
useEffect(() => {
// 🚨 이렇게 해도 버그가 해결되지 않음!
// "✅ 연결 중..."이 한 번만 표시되지만 버그는 해결되지 않음
if (!connectionRef.current) {
connectionRef.current = createConnection();
connectionRef.current.connect();
}
}, []);
// 버그를 수정하려면 이펙트를 1번 실행하는 것만으로는 충분하지 않음
// 다시 마운트한 후에도 효과가 작동해야 하므로 위에서 언급한 해결 방법처럼 연결을 정리해야 함
데이터 페칭
만약 Effect가 어떤 데이터를 가져온다면, 클린업 함수에서는 fetch를 중단하거나 결과를 무시해야 한다.
useEffect(() => {
let ignore = false;
async function startFetching() {
const json = await fetchTodos(userId);
if (!ignore) {
setTodos(json);
}
}
startFetching();
return () => {
ignore = true;
};
}, [userId]);
개발 중에는 네트워크 탭에서 두 개의 fetch가 표시된다.
이는 문제가 없다. 위의 접근 방식을 사용하면 첫 번째 Effect는 즉시 클린업되어 ignore 변수의 복사본이 true로 설정된다. 따라서 추가 요청이 있더라도 if (!ignore) 검사 덕분에 state에 영향을 미치지 않는다.
제품 환경에서는 하나의 요청만 있을 것이다.
개발 중에 두 번째 요청이 문제라면, 가장 좋은 방법은 중복 요청을 제거하고 컴포넌트 간에 응답을 캐시하는 솔루션을 사용해야 한다.
function TodoList() {
const todos = useSomeDataLibrary(`/api/user/${userId}/todos`);
이렇게 하면 개발 환경을 개선하는데 도움이 될 뿐만 아니라 애플리케이션의 반응 속도도 향상된다. 예를 들어, 사용자가 뒤로 가기 버튼을 눌렀을 때 데이터를 다시 로드하는 것을 기다릴 필요가 없다. 데이터가 캐시되기 때문이다.
💡 [중요] Effect에서 데이터를 가져오는 좋은 대안은 무엇인가?
Effect 안에서 fetch 호출을 작성하는 것은 데이터를 가져오는 인기 있는 방법이다.
특히 완전히 클라이언트 측 앱에서는 말이다.
하지만 이는 매우 수동적인 접근 방식이며 중요한 단점이 있다.
- Effect는 서버에서 실행되지 않는다. 따라서, 초기 서버 렌더링된 HTML은 데이터가 없는 로딩 상태만 포함하게 된다. 클라이언트 컴퓨터는 모든 JavaScript를 다운로드하고 앱을 렌더링해야만 데이터를 로드해야 한다는 것을 알게 될 것이다. 이는 효율적이지 않다.
- Effect 안에서 직접 가져오면 ‘네트워크 폭포’를 쉽게 만들 수 있다. 부모 컴포넌트를 렌더링하면 일부 데이터를 가져오고, 자식 컴포넌트를 렌더링한 다음 그들이 데이터를 가져오기 시작한다. 네트워크가 빠르지 않으면 이는 모든 데이터를 병렬로 가져오는 것보다 훨씬 느리다.
- Effect 안에서 직접 가져오는 건 일반적으로 데이터를 미리 로드하거나 캐싱하지 않음을 의미한다. 예를 들어 컴포넌트가 언마운트되고 다시 마운트되면 데이터를 다시 가져와야 한다.
- 그리 편리하지 않다. fetch 호출을 작성할 때 경쟁 상태와 같은 버그에 영향을 받지 않는 방식으로 작성하는 데 꽤 많은 보일러플레이트 코드가 필요하다.
위 단점들은 React에만 해당되는 것이 아니다.
어떤 라이브러리에서든 마운트 시에 데이터를 가져온다면 비슷한 단점이 존재한다. 마운트 시에 데이터를 페칭하는 것도 라우팅과 마찬가지로 잘 수행하기 어려운 작업이므로 다음 접근 방식을 권장한다.
- 프레임워크를 사용하는 경우, 해당 프레임워크의 내장 데이터 페칭 메커니즘을 사용하자. 현대적인 React 프레임워크에는 위의 단점을 겪지 않는 효율적이고 통합적인 데이터 페칭 메커니즘이 포함되어 있다.
- 그렇지 않은 경우, 클라이언트 측 캐시를 사용하거나 구축하는 것을 고려하자. 인기 있는 오픈 소스 솔루션으로는 React Query, useSWR 및 React Router 6.4+이 있다. 직접 솔루션을 구축할 수도 있다. 이 경우, Effect를 내부적으로 사용하면서 요청 중복을 제거하고 응답을 캐시하고 네트워크 폭포를 피하는 로직을 추가할 것이다. (데이터를 사전에 로드하거나 데이터 요구 사항을 route)
이러한 접근 방식 중 어느 것도 적합하지 않은 경우, Effect 내에서 데이터를 직접 가져오는 것을 계속해도 된다.
'[Front-end] 개발자 공부' 카테고리의 다른 글
[개발 공부 81일차] React 공식 문서 | Custom Hook으로 로직 재사용하는 법 (0) | 2024.07.12 |
---|---|
[개발 공부 80일차] React 공식 문서 | Escape Hatches (4) (0) | 2024.07.11 |
[개발 공부 78일차] React 공식 문서 | Escape Hatches (2) (0) | 2024.07.09 |
[개발 공부 77일차] React 공식 문서 | Escape Hatches (1) (0) | 2024.07.08 |
[개발 공부 76일차] 스크랩 여부가 공유되는 문제 해결 (4) | 2024.07.02 |