개발일지
useEffect 본문
📌 useEffect란?
useEffect는 컴포넌트를 외부 시스템과 동기화할 수 있는 React훅입니다.
useEffect(setup, dependencies?)
📌 useEffect
컴포넌트의 최상위 레벨에서 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에서 제거되면, React는 마지막으로 클린업 함수를 실행합니다.
- 선택적 setup: setup 코드 내에서 참조된 모든 반응형 값의 목록입니다. 반응형 값은 props, state, 컴포넌트 본문 내부에서 직접 선언한 모든 변수와 함수를 포함합니다. React용으로 구성된 린터는 모든 반응형 값이 의존성에 잘 지정되었는지 확인합니다. 의존성 목록에는 고정된 수의 항목이 있어야 하며 [dep1, dep2, dep3]과 같이 인라인으로 작성해야 합니다. React는 각 의존성에 대해 Object.is로 이전 값과 비교합니다. 의존성을 전혀 지정하지 않으면 컴포넌트를 다시 렌더링 할 때마다 useEffect가 실행됩니다.
📍 반환값
useEffect는 undefined를 반환합니다.
📍 주의사항
- useEffect는 훅이므로 컴포넌트의 최상위 레벨 또는 자체 훅에서만 호출할 수 있습니다. 반복문이나 조건문 내부에서는 호출할 수 없습니다.
- 외부 시스템과 동기화하려는 목적이 아니라면 Effect가 필요하지 않을지도 모릅니다.
- Strict 모드가 켜져 있으면 React는 첫 번째 실제 셋업 전에 개발전용의 셋업+클린업 사이클을 한번 더 실행합니다. 이는 클린업 로직이 셋업 로직을 "미러링"하고 셋업이 수행 중인 모든 작업을 중지하거나 취소하는지를 확인하는 스트레스 테스트입니다.
- 의존성 중 일부가 컴포넌트 내부에 정의된 객체 또는 함수인 경우 Effect가 필요 이상으로 자주 다시 실행될 위험이 있습니다. 이 문제를 해결하려면 불필요한 객체 및 함수 의존성을 제거하세요.
- Effect가 상호작용(ex: 클릭)으로 인한 것이 아니라면, React는 브라우저가 Effect를 실행하기 전에 업데이트를 처리하기 전에 화면을 다시 그릴 수 있습니다. 보통 이게 기대하는 동작일 것입니다. 만약 브라우저가 화면을 다시 칠하지 못하도록 차단해야 하는 경우라면 useEffect를 useLayoutEffect로 바꿔야 합니다.
- Effect는 클라이언트에서만 실행됩니다. 서버 렌더링 중에는 실행되지 않습니다.
📌 사용방법
1️⃣ 외부 시스템에 연결하기
때로는 컴포넌트가 페이지에 표시되는 동안 네트워크, 일부 브라우저 API 또는 타사 라이브러리에 연결 상태를 유지해야 할 수도 있습니다.
이러한 시스템은 React에서 제어되지 않으므로 외부라고 합니다.
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. 해당 시스템에 연결하는 셋업 코드가 포함된 셋업 함수
- 해당 시스템과의 연결을 끊는 클린업 코드가 포함된 클린업 함수를 반환해야 합니다.
2. 해당 함수 내부에서 사용되는 컴포넌트의 모든 값을 포함한 의존성 목록.
React는 필요할 때마다 셋업 및 클리업 함수를 호출하는데, 이는 여러 번 발생할 수 있습니다.
1. 컴포넌트가 페이지에 추가될 때(마운트)마다 셋업 코드를 실행합니다.
2. 의존성이 변경된 컴포넌트를 다시 렌더링 할 때마다:
- 먼저 이전 props와 state로 클린업 코드를 실행합니다.
- 그런 다음 새 props와 state로 셋업 코드를 실행합니다.
3. 컴포넌트가 페이지에서 제거되면(마운트 해제) 마지막으로 한번 클리업 코드를 실행합니다.
2️⃣ 커스텀 훅으로 Effect 감싸기
Effect는 "탈출구"입니다. React를 벗어나야 할 때, 또는 더 나은 빌트인 솔루션이 없을 때 사용합니다.
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
});
// ...
3️⃣ React가 아닌 위젯 제어하기
외부 시스템을 컴포넌트의 특정 prop이나 state와 동기화를 하고 싶을 때가 있습니다.
예를 들어, React 없이 작성된 타사 맵 위젯이나 비디오 플레이어 컴포넌트가 있는 경우, Effect를 사용하여 해당 state를 React 컴포넌트의 현재 state와 일치시키는 메서드를 호출할 수 있습니다.
4️⃣ Effect로 데이터 페칭하기
Effect를 사용하거나 컴포넌트에 대한 데이터를 패치할 수 있습니다. 프레임워크를 사용하는 경우, 프레임워크의 데이터 페칭 메커니즘을 사용하는 것이 Effects를 수동으로 작성하는 것보다 훨씬 효율적이라는 점에 유의하세요.
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로 설정됩니다. 이렇게 하면 네트워크 응답이 보낸 순서와 다른 순서로 도착하더라도 '조건경합'이 발생하지 않습니다.
Effects에서 직접 데이터를 페칭 하는 작업을 반복적으로 작성하면 나중에 캐싱 및 서버 렌더링과 같은 최적화를 추가하기가 어려워집니다.
5️⃣ 반응형 의존성 지정
Effect의 의존성을 "선택"할 수 없다는 점에 유의하세요. 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은 반응형 값이기 때문에 의존성 목록에서 제거할 수 없습니다. 만약 이 값을 생략하려고 할 때 린터가 React용으로 올바르게 구성되어 있다면, 린터는 이를 수정해야 하는 실수로 표시해 줍니다:
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가 변경되어도 다시 실행되지 않습니다.
6️⃣ 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을 다시 클린업하고 셋업 할 필요가 없습니다.
7️⃣ 불필요한 객체 의존성 제거하기
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 내에서 객체를 생성하세요:
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]);
이제 Effect 내부에 options 객체를 만들었으므로 Effect는 오직 roomId 문자열에마 ㄴ의존하게 되었습니다.
이 수정 덕에 input에 타이핑을 해도 채팅이 다시 연결되지 않습니다. 다시 만들어지는 객체와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다.
8️⃣ 불필요한 함수 의존성 제거하기
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 내에서 선언하세요:
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]);
이제 Effect 내에서 createOptions 함수를 정의하면 Effect 자체는 roomId 문자열에만 의존합니다. 이 수정으로 input에 타이핑해도 채팅이 다시 연결되지 않습니다. 새로 생성되는 함수와 달리 roomId와 같은 문자열은 다른 값으로 설정하지 않는 한 변경되지 않습니다.
'리액트 공식문서 스터디' 카테고리의 다른 글
useEffect, useLayoutEffect 발표 자료 (1) | 2024.06.13 |
---|---|
useLayoutEffect (0) | 2024.06.11 |
2회차 발표 (0) | 2024.05.31 |
useRef (0) | 2024.05.30 |
useContext (0) | 2024.05.28 |