1. 불필요한 Effect를 제거하는 방법
Effect가 필요하지 않은 흔한 경우는 두 가지가 있다.
렌더링을 위해 데이터를 변환하는 경우 Effect는 필요하지 않다.
예를 들어, 목록을 표시하기 전에 필터링하고 싶다고 가정해 보자.
목록이 변경될 때 state 변수를 업데이트하는 Effect를 작성하고 싶을 수 있다.
하지만 이는 비효율적이다.
컴포넌트의 state를 업데이트할 때 React는 먼저 컴포넌트 함수를 호출해 화면에 표시될 내용을 계산한다.
다음으로 이러한 변경 사항을 DOM에 “commit”하여 화면을 업데이트하고, 그 후에 Effect를 실행한다.
만약 Effect “역시” state를 즉시 업데이트한다면, 이로 인해 전체 프로세스가 처음부터 다시 시작될 것이다.
불필요한 렌더링을 피하려면 모든 데이터 변환을 컴포넌트의 최상위 레벨에서 하라.
그러면 props나 state가 변경될 때마다 해당 코드가 자동으로 다시 실행될 것아다.
사용자 이벤트를 처리하는 데에 Effect는 필요하지 않다.
예를 들어, 사용자가 제품을 구매할 때 /api/buy POST 요청을 전송하고 알림을 표시하고 싶다고 하자.
구매 버튼 클릭 이벤트 핸들러에서는 정확히 어떤 일이 일어났는지 알 수 있다.
반면 Effect는 사용자가 무엇을 했는지(예: 어떤 버튼을 클릭했는지)를 알 수 없다.
그렇기 때문에 일반적으로 사용자 이벤트를 해당 이벤트 핸들러에서 처리한다.
한편 외부 시스템과 동기화하려면 Effect가 필요하다.
예를 들어, jQuery 위젯을 React state와 동기화하는 Effect를 작성할 수 있다.
또한 검색 결과를 현재의 검색 쿼리와 동기화하기 위해 데이터 요청을 Effect로 처리할 수 있다.
최신 프레임워크는 컴포넌트에 직접 Effects를 작성하는 것보다 더 효율적인 빌트인 데이터 페칭 메커니즘을 제공한다는 점을 명심하라.
1-1. props 또는 state에 따라 state 업데이트하기
1-2. 복잡한 계산 캐싱하기
todos
를 filter
prop에 따라 필터링하여 visibleTodos
를 계산한다.function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// 🔴 중복 state 및 불필요한 Effect 사용을 자제하자.
const [visibleTodos, setVisibleTodos] = useState([]);
useEffect(() => {
setVisibleTodos(getFilteredTodos(todos, filter));
}, [todos, filter]);
// ...
}
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ getFilteredTodos()가 느리지 않다면 괜찮다.
const visibleTodos = getFilteredTodos(todos, filter);
// ...
}
getFilteredTodos()
가 느리거나 todos
가 많을 경우, newTodo
와 같이 관련 없는 state 변수가 변경되더라도 getFilteredTodos()
를 다시 계산하고 싶지 않을 수 있다.
이럴 땐 복잡한 계산을 useMemo
훅으로 감싸서 캐시(또는 “메모화 (memoize)”)할 수 있다.
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
const visibleTodos = useMemo(() => {
// ✅ todos나 filter가 변하지 않는 한 재실행되지 않음
return getFilteredTodos(todos, filter);
}, [todos, filter]);
// ...
}
또는 아래와 같이 한 줄로 작성할 수도 있다.
import { useMemo, useState } from 'react';
function TodoList({ todos, filter }) {
const [newTodo, setNewTodo] = useState('');
// ✅ todos나 filter가 변하지 않는 한 getFilteredTodos()가 재실행되지 않음
const visibleTodos = useMemo(() => getFilteredTodos(todos, filter), [todos, filter]);
// ...
}
todos
나 filter
가 변경되지 않는 한 내부 함수가 다시 실행되지 않기를 원한다는 것을 React에 알린다.getFilteredTodos()
의 반환값을 기억한다.useMemo
는 마지막으로 저장한 결과를 반환한다.useMemo
로 감싸는 함수는 렌더링 중에 실행되므로, 순수 계산에만 작동한다.1-3. prop이 변경되면 모든 state 재설정하기
ProfilePage
컴포넌트는 userId
prop을 받는다.comment
state 변수를 사용하여 그 값을 보관한다.userId
가 변경될 때마다 comment
state 변수를 지워줘야 한다.export default function ProfilePage({ userId }) {
const [comment, setComment] = useState('');
// 🔴 prop 변경시 Effect에서 state 재설정 수행하므로 자제
useEffect(() => {
setComment('');
}, [userId]);
// ...
}
ProfilePage
와 그 자식들이 먼저 오래된 값으로 렌더링한 다음 새로운 값으로 다시 렌더링하기 때문에 비효율적이다.ProfilePage
내부에 어떤 state가 있는 모든 컴포넌트에서 이 작업을 수행해야 하므로 복잡하다.key
속성을 전달하라.export default function ProfilePage({ userId }) {
return (
<Profile
userId={userId}
key={userId}
/>
);
}
function Profile({ userId }) {
// ✅ key가 변하면 이 컴포넌트 및 모든 자식 컴포넌트의 state가 자동으로 재설정됨
const [comment, setComment] = useState('');
// ...
}
userId
를 key
로 Profile
컴포넌트에 전달하는 것은 곧, userId
가 다른 두 Profile
컴포넌트를 state를 공유하지 않는 별개의 컴포넌트들로 취급하도록 React에게 요청하는 것이다.userId
로 설정한) key
가 변경될 때마다 DOM을 다시 생성하고 state를 재설정하며, Profile
컴포넌트 및 모든 자식들의 state를 재설정할 것이다.comment
필드는 프로필들을 탐색할 때마다 자동으로 지워진다.ProfilePage
컴포넌트만 export하였으므로 프로젝트의 다른 파일에서는 오직 ProfilePage
컴포넌트에만 접근 가능하다.ProfilePage
를 렌더링하는 컴포넌트는 key
를 전달할 필요 없이 일반적인 prop
으로 userId
만 전달하고 있다.ProfilePage
가 내부의 Profile
컴포넌트에 key
로 전달한다는 사실은 내부에서만 알고 있는 구현 세부 사항이다.1-4. props가 변경될 때 일부 state 조정하기
prop
이 변경될 때 state의 전체가 아닌 일부만 재설정하거나 조정하고 싶을 수 있다.List
컴포넌트는 items
목록을 prop
으로 받고, selection
state 변수에 선택된 항목을 유지한다.items prop
이 다른 배열을 받을 때마다 selection
을 null
로 재설정하고 싶다.function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// 🔴 prop 변경시 Effect에서 state 조정 자제
useEffect(() => {
setSelection(null);
}, [items]);
// ...
}
items
가 변경될 때마다 List
와 그 하위 컴포넌트는 처음에는 오래된 selection
값으로 렌더링된다.setSelection(null)
호출은 List
와 그 자식 컴포넌트를 다시 렌더링하여 이 전체 과정을 재시작하게 된다.function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selection, setSelection] = useState(null);
// ✅ 렌더링 중에 state 조정하는 것이 낫다.
const [prevItems, setPrevItems] = useState(items);
if (items !== prevItems) {
setPrevItems(items);
setSelection(null);
}
// ...
}
setSelection
이 직접 호출된다.return
문과 함께 종료된 직후에 List를 다시 렌더링한다.List
의 자식들을 렌더링하거나 DOM을 업데이트하지 않았기 때문에, List
의 자식들은 기존의 selection
값에 대한 렌더링을 건너뛰게 된다.items !== prevItems
와 같은 조건이 필요한 것이다.props
나 다른 state들을 바탕으로 state를 조정하면 데이터 흐름을 이해하고 디버깅하기 어려워질 것이다.key
로 모든 state를 재설정하거나 렌더링 중에 모두 계산할 수 있는지를 확인하라.function List({ items }) {
const [isReverse, setIsReverse] = useState(false);
const [selectedId, setSelectedId] = useState(null);
// ✅ 가장 좋음: 렌더링 중에 모든 값을 계산
const selection = items.find(item => item.id === selectedId) ?? null;
// ...
}
selection
항목은 일치하는 항목을 찾지 못하므로 null
이 된다.items
에 대한 대부분의 변경과 무관하게 ‘selection’ 항목은 그대로 유지되므로 대체로 더 나은 방법이다.1-5. 이벤트 핸들러 간 로직 공유
1-6. POST요청 보내기
1-7. 연쇄 계산
1-8. 애플리케이션 초기화하기
1-9. state변경을 부모 컴포넌트에 알리기
1-10. 부모에게 데이터 전달하기
1-11. 외부 스토어 구독하기
1-12. 데이터 페칭하기
https://developer.mozilla.org/ko/