일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- JavaScript
- 내일배움캠프 프로젝트
- 파이썬 반복문
- 타입스크립트 리액트
- 프로그래머스
- 타입스크립트 props
- 리액트 프로젝트
- 파이썬 for in
- 파이썬 slice
- 한글 공부 사이트
- typeScript
- 타입스크립트
- 리액트
- 파이썬 replace
- 파이썬 딕셔너리
- useEffect
- 파이썬 enumerate
- tanstack query
- 내일배움캠프 최종 프로젝트
- 리액트 훅
- Next 팀 프로젝트
- 내일배움캠프
- 내배캠 프로젝트
- 파이썬 for
- React Hooks
- 코딩테스트
- 리액트 공식문서
- REACT
- 자바스크립트
- useState
- Today
- Total
sohyeon kim
[React] 리액트 공식문서 정리 4(4) : 탈출구 - useEffect 의존성 배열에서 불필요한 요소 제거하기 본문
💡 리액트 공식문서 정리 : 탈출구 - Effect 의존성 제거하기
effect 를 작성하면 린터는 effect 의 의존성 배열에 effect 가 읽는 모든 반응형 값(props 나 state 등)을 포함했는지 확인한다.
린터에 따라 반응형 값을 의존성 배열에 포함하면 effect 가 컴포넌트의 최신 props 및 state 와 동기화 상태를 유지할 수 잇게 된다.
하지만 불필요한 의존성으로 인해 effect 가 너무 자주 실행되거나 무한 루프를 생성할 때도 있다.
effect 에서 불필요한 의존성을 검토하고 제거하는 방법에 대해 알아보자.
1️⃣ 의존성은 항상 코드와 일치해야 한다.
effect 를 작성할 땐 먼저 effect 가 수행하기를 원하는 작업을 시작하고 중지하는 방법을 지정한다.
그 다음 effect 의존성 배열을 비워두면([]) 린터가 올바른 의존성을 제안한다.
린터에 표시된 내용에 따라 의존성 배열을 채우자.
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(serverUrl, roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
// ...
}
Effect 는 반응형 값에 '반응'한다.
- roomId 는 반응형 값이므로(리렌더링으로 인해 변경될 수 있음) 린터는 이를 의존성으로 지정했는지 확인한다.
- roomId 가 다른 값을 받으면 React 는 Effect 를 다시 동기화한다.
- 그럼 채팅이 선택된 방에 연결된 상태를 유지하고 드롭다운에 '반응'한다.
🚨 린터를 억제한다면, 매우 혼란스러운 버그가 발생하므로 피해야 한다.
2️⃣ 의존성을 제거하려면 의존성이 아님을(린터에게 해당 의존성이 필요하지 않음을) 증명하라.
effect 의 의존성은 '선택'할 수 없다.
effect 의 코드에서 사용되는 모든 반응형 값은 의존성 배열에 선언되어야 하며, 이 배열은 주변 코드에 의해 결정된다.
위 코드에서 의존성을 제거하려면
예시로, roomId 를 컴포넌트 밖으로 이동시켜 반응형 값이 아니며, 리렌더링 시에도 변경되지 않음을 증명할 수 있다.
const serverUrl = 'https://localhost:1234';
const roomId = 'music'; // Not a reactive value anymore
function ChatRoom() {
useEffect(() => {
// ...
}, []); // ✅ All dependencies declared
// ...
}
3️⃣ 의존성을 변경하려면 코드를 변경하라.
- 먼저 effect 의 코드 or 반응형 값 선언 방식을 변경한다.
- 그 다음, 변경한 코드에 맞게 의존성을 조정한다.
- 의존성 배열이 마음에 들지 않으면 첫 단계로 돌아간다.(그리고 코드를 다시 수정한다.)
마지막 부분이 중요하다. 의존성을 변경하려면 먼저 주변 코드를 변경하라.
의존성 배열은 effect 의 코드에서 사용하는 모든 반응형 값의 목록이라고 생각하면 된다. 이 배열에 무엇을 넣을 지는 사용자가 선택하지 않는다. 의존성 제거란 목표를 설정햇다면, 그 목표에 맞는 코드를 '찾아야' 한다.
4️⃣ 불필요한 의존성 제거하기
의존성 배열에 불필요한 값이 포함된 경우와 해결책을 찾기 위한 질문들을 살펴보자.
- 다른 조건에서 effect 의 다른 부분을 다시 실행하고 싶은 경우
- 일부 의존성의 변경에 '반응'하지 않고 '최신' 값만 읽고 싶은 경우
- (의존성은 객체나 함수이기 때문에) 의도치 않게 너무 자주 변경되는 경우
이 코드를 이벤트 핸들러로 옮겨야 하나?
가장 먼저 고려해야 할 것은 이 코드가 effect 가 되어야 하는지 여부이다.
특정 상호작용에 대한 응답으로 코드를 실행하려면 해당 로직은 해당 이벤트 핸들러에 넣어야 한다.
Effect 가 서로 관련이 없는 여러 가지 작업을 수행하나?
사용자가 도시와 지역을 선택해야 하는 배송 폼을 만든다고 가정해 보자. 선택한 country 에 따라 서버에서 cities 목록을 가져와 드롭다운에 표시한다. form 이 표시되는 즉시 그리고 country 가 변경될 때마다 데이터를 가져와야 하므로 이는 effect 에서 실행된다. 또한 effect 가 city state 변수를 사용하므로 의존성 배열에 city 를 추가한다.
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/cities?country=${country}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setCities(json);
}
});
// 🔴 Avoid: A single Effect synchronizes two independent processes
if (city) {
fetch(`/api/areas?city=${city}`)
.then(response => response.json())
.then(json => {
if (!ignore) {
setAreas(json);
}
});
}
return () => {
ignore = true;
};
}, [country, city]); // ✅ All dependencies declared
// ...
의존성 배열에 두 값이 포함되어 사용자가 다른 도시를 선택하면 effect 가 재실행되어 fetchCities(country) 를 호출하는 문제가 발생했다. 불필요하게 도시 목록을 여러 번 가져오고 있다.
위 코드의 문제점은 서로 관련이 없는 두 가지를 동기화하고 있다는 것
- country props 를 기반으로 cities state 를 네트워크에 동기화하려고 한다.
- city state 를 기반으로 areas state 를 네트워크게 동기화하려고 한다.
로직을 두 개의 effect 로 분리하고, 각 effect 는 동기화해야 하는 props 에 반응해야 한다.
function ShippingForm({ country }) {
const [cities, setCities] = useState(null);
useEffect(() => {
// ...
};
}, [country]); // ✅ All dependencies declared
const [city, setCity] = useState(null);
const [areas, setAreas] = useState(null);
useEffect(() => {
if (city) {
// ...
}
}, [city]); // ✅ All dependencies declared
// ...
이제 첫 번째 effect 는 country 가 변경될 때만, 두 번째는 cityr 가 변경될 때만 다시 실행된다. 목적에 따라 분리했으니, 서로 다른 두 가지가 두 개의 개별 effect 에 의해 동기화된다. 각각의 effect 에는 각각의 의존성 배열이 있으므로 의도치 않게 서로를 트리거하지 않는다.
최종 코드는 이전 코드보다 길지만 effect 를 분리하는 것이 명확한 코드이다. 각 effect 는 독립적인 동기화 프로세스를 나타내야 한다.
이 예시에선 한 effect 를 삭제해도 다른 effect 의 로직이 깨지지 않는다. 즉, 서로 다른 것을 동기화하므로 분할하는 것이 좋다.
중복이 걱정된다면 반복되는 로직을 커스텀 훅으로 추출해 코드를 개선할 수 있다. 🔗
다음 state 를 계산하기 위해 어떤 state 를 읽고 있나?
아래 effect 는 새 메시지가 도착할 때마다 새로 생성된 배열로 messages state 를 업데이트한다. messages 변수를 사용해 기존 메시지로 시작하는 새 배열을 생성하고 마지막에 추가한다. message 는 effect 에서 읽는 반응형 값이므로 의존성이어야 한다.
function ChatRoom({ roomId }) {
const [messages, setMessages] = useState([]);
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages([...messages, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId, messages]); // ✅ All dependencies declared
// ...
이 때 문제가 발생한다. 메시지를 수신할 때마다 setMessages() 는 컴포넌트가 수신된 메시지를 포함하는 새 messages 배열로 리렌더링하도록 한다. 하지만 이 effect 는 이제 messages 에 따라 달라지므로 effect 도 다시 동기화된다. 따라서 새 메시지가 올 때마다 채팅이 다시 연결된다. 이를 해결하려면 effect 내에서 messages 를 읽지 말고, 업데이터 함수를 setMessages 에 전달하자.
useEffect(() => {
const connection = createConnection();
connection.connect();
connection.on('message', (receivedMessage) => {
setMessages(msgs => [...msgs, receivedMessage]);
});
return () => connection.disconnect();
}, [roomId]); // ✅ All dependencies declared
이제 effect 는 message 변수에 전혀 의존하지 않고, React 는 업데이터 함수를 대기열에 넣고 다음 렌더링 중 msgs 인수를 제공한다.
이 덕분에 메시지를 수신해도 더 이상 채팅이 다시 연결되지 않는다.
값의 변경에 '반응'하지 않고 값을 읽고 싶은가?
useEffectEvent API 활용(개발 중) 🔗
일부 반응형 값이 의도치 않게 변경되나?
effect 가 특정 값에 '반응'하기를 원하지만, 그 값이 생각보다 더 자주 변경되어 사용자 관점에서 실제 변경 사항을 반영하지 못할 수도 있다. 예시로, 컴포넌트에 options 객체를 생성하고, effect 내에서 해당 객체를 읽는다고 가정해 보자.
function ChatRoom({ roomId }) {
const [message, setMessage] = useState('');
const options = {
serverUrl: serverUrl,
roomId: roomId
};
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
return (
<>
<h1>Welcome to the {roomId} room!</h1>
<input value={message} onChange={e => setMessage(e.target.value)} />
</>
);
}
이 객체는 컴포넌트 내에서 선언되므로 반응형 값이다. effect 내에서 이 값을 읽으면 의존성으로 선언해야 하고, effect 는 그 값에 '반응'하게 된다. 그럼 roomId 가 변경된다면 effect 가 새 options 로 채팅에 다시 연결한다.
하지만 문제가 발생한다. 위 코드에서 input 입력은 message state 만 업데이트 한다. 사용자 관점에서 이는 채팅 연결과 아무 상관이 없다. 하지만 message 를 업데이트할 때마다 컴포넌트가 리렌더링되고 그 안에 있는 코드가 처음부터 다시 실행된다.
ChatRoom 컴포넌트를 리렌더링할 때마다 새 options 객체가 생성된다. React 는 options 가 마지막 렌더링 중 생성된 options 와 다른 객체임을 인식한다. 그러므로 (options 에 따라 달라지는) effect 를 다시 동기화하고 사용자가 입력할 때 채팅이 다시 연결된다.
이 문제는 개체와 함수에만 영향을 준다. JS 에서는 새로 생성된 객체와 함수가 다른 모든 객체와 구별되는 것으로 간주된다.
그 안의 내용이 동일할 수 있다는 것은 아무 상관이 없다.
// During the first render
const options1 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// During the next render
const options2 = { serverUrl: 'https://localhost:1234', roomId: 'music' };
// These are two different objects!
console.log(Object.is(options1, options2)); // false
객체 및 함수 의존성으로 인해 effect 가 필요 이상으로 자주 재동기화될 수 있다.
그렇기에 가능하면 객체와 함수를 effect 의 의존성으로 사용하지 않는 것이 좋다. 대신 컴포넌트 외부나 effect 내로 이동하거나 원시 값을 추출해 보자.
정적 객체와 함수를 컴포넌트 외부로 이동
객체가 props 및 state 에 의존하지 않는 경우 해당 객체를 컴포넌트 외부로 이동할 수 있다. 그럼 린터에게 반응형 값이 아닌 것을 알려주므로 의존성이 될 필요가 없다. 이제 ChatRoom 을 재렌더링해도 effect 가 다시 동기화되지 않는다.
const options = {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
이는 함수에도 적용된다.
function createOptions() {
return {
serverUrl: 'https://localhost:1234',
roomId: 'music'
};
}
function ChatRoom() {
const [message, setMessage] = useState('');
useEffect(() => {
const options = createOptions();
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, []); // ✅ All dependencies declared
// ...
Effect 내에서 동적 객체 및 함수 이동
객체가 roomId props 처럼 리렌더링의 결과로 변경될 수 있는 반응형 값에 의존한다면, 컴포넌트 외부로 끌어낼 수 없다.
하지만 effect 의 내부로 이동시킬 순 있다. (함수도 마찬가지)
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]); // ✅ All dependencies declared
// ...
이제 options 는 effect 내부에 있으므로 더 이상 effect 의 의존성이 아니게 된다. 대신 effect 에서 사용하는 반응형 값은 roomId 이다. 이는 객체나 함수가 아니기 때문에 의도치 않게 달라지지 않을 것이라 확신할 수 있다. JS 에서 숫자와 문자열은 그 내용에 따라 구분된다.
// During the first render
const roomId1 = 'music';
// During the next render
const roomId2 = 'music';
// These two strings are the same!
console.log(Object.is(roomId1, roomId2)); // true
위 수정 덕분에 입력을 수정해도 더 이상 채팅은 다시 연결되지 않으며, roomId 변경 시에만 재연결된다.
객체에서 원시 값 읽기
가끔 props 에서 객체를 받을 수도 있다. 이는 렌더링 중 부모 컴포넌트가 객체를 생성한다는 점에서 위험하다.
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
useEffect(() => {
const connection = createConnection(options);
connection.connect();
return () => connection.disconnect();
}, [options]); // ✅ All dependencies declared
// ...
<ChatRoom
roomId={roomId}
options={{
serverUrl: serverUrl,
roomId: roomId
}}
/>
이렇게 하면 부모가 리렌더링될 때마다 effect 가 다시 연결된다. 이를 해결하기 위해 effect 외부의 객체에서 정보를 읽고 객체 및 함수의존성을 피하자.
function ChatRoom({ options }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = options;
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
이 로직은 약간 반복적이다. effect 외부 객체에서 일부 값을 읽은 다음 effect 내부에 동일 값을 가진 객체를 만들고 있다.
하지만 effect 가 실제로 어떤 정보에 의존하는지 명확하게 알 수 있다. 부모에 의해 의도치 않게 객체가 다시 생성된 경우 채팅이 다시 연결되지 않으며, options.roomId 또는 options.serverUrl 이 실제로 다른 경우 채팅이 재연결된다.
함수에서 원시값 계산
함수에 대해서도 동일한 접근 방식을 사용할 수 있다. 부모 컴포넌트가 함수를 전달한다고 가정해 보자.
<ChatRoom
roomId={roomId}
getOptions={() => {
return {
serverUrl: serverUrl,
roomId: roomId
};
}}
/>
의존성을 만들지 않으려면(리렌더링 시 재연결을 방지하려면) effect 외부에서 호출하자.
이렇게 하면 객체가 아니며 effect 내부에서 읽을 수 있는 roomId 및 serverUrl 값을 얻을 수 있다.
function ChatRoom({ getOptions }) {
const [message, setMessage] = useState('');
const { roomId, serverUrl } = getOptions();
useEffect(() => {
const connection = createConnection({
roomId: roomId,
serverUrl: serverUrl
});
connection.connect();
return () => connection.disconnect();
}, [roomId, serverUrl]); // ✅ All dependencies declared
// ...
이는 렌더링 중에 호출해도 안전하므로 순수 함수에서만 작동한다. 함수가 이벤트 핸들러이지만 변경 사항으로 인한 effect 의 재동기화를 원화지 않는다면, 대신 effect 이벤트로 함수를 감싸자. 🔗
🔗 https://ko.react.dev/learn/removing-effect-dependencies
Effect 의존성 제거하기 – React
The library for web and native user interfaces
ko.react.dev