React 공식문서 이해하기 (26)

Syoee·2023년 12월 6일
0

React

목록 보기
26/30
post-thumbnail

Chapter 4. Escape Hatches

#4 Effect가 필요하지 않을 수도 있다.

학습 목차

1. 불필요한 Effect를 제거하는 방법


1. 불필요한 Effect를 제거하는 방법

  • Effect가 필요하지 않은 흔한 경우는 두 가지가 있다.

    1. 렌더링을 위해 데이터를 변환하는 경우 Effect는 필요하지 않다.
      예를 들어, 목록을 표시하기 전에 필터링하고 싶다고 가정해 보자.
      목록이 변경될 때 state 변수를 업데이트하는 Effect를 작성하고 싶을 수 있다.
      하지만 이는 비효율적이다.
      컴포넌트의 state를 업데이트할 때 React는 먼저 컴포넌트 함수를 호출해 화면에 표시될 내용을 계산한다.
      다음으로 이러한 변경 사항을 DOM에 “commit”하여 화면을 업데이트하고, 그 후에 Effect를 실행한다.
      만약 Effect “역시” state를 즉시 업데이트한다면, 이로 인해 전체 프로세스가 처음부터 다시 시작될 것이다.
      불필요한 렌더링을 피하려면 모든 데이터 변환을 컴포넌트의 최상위 레벨에서 하라.
      그러면 props나 state가 변경될 때마다 해당 코드가 자동으로 다시 실행될 것아다.

    2. 사용자 이벤트를 처리하는 데에 Effect는 필요하지 않다.
      예를 들어, 사용자가 제품을 구매할 때 /api/buy POST 요청을 전송하고 알림을 표시하고 싶다고 하자.
      구매 버튼 클릭 이벤트 핸들러에서는 정확히 어떤 일이 일어났는지 알 수 있다.
      반면 Effect는 사용자가 무엇을 했는지(예: 어떤 버튼을 클릭했는지)를 알 수 없다.
      그렇기 때문에 일반적으로 사용자 이벤트를 해당 이벤트 핸들러에서 처리한다.

  • 한편 외부 시스템과 동기화하려면 Effect가 필요하다.
    예를 들어, jQuery 위젯을 React state와 동기화하는 Effect를 작성할 수 있다.
    또한 검색 결과를 현재의 검색 쿼리와 동기화하기 위해 데이터 요청을 Effect로 처리할 수 있다.
    최신 프레임워크는 컴포넌트에 직접 Effects를 작성하는 것보다 더 효율적인 빌트인 데이터 페칭 메커니즘을 제공한다는 점을 명심하라.

1-1. props 또는 state에 따라 state 업데이트하기

  • 기존 props나 state에서 계산할 수 있는 것이 있으면 state에 넣지 말자.
    대신 렌더링 중에 계산하라.
    이렇게 하면 코드가 더 빨라지고, (추가적인 “계단식” 업데이트를 피함)
    더 간단해지고, (일부 코드 제거)
    오류가 덜 발생한다. (서로 다른 state 변수가 서로 동기화되지 않아 발생하는 버그를 피함) 이 접근 방식이 생소하게 느껴진다면, React로 사고하기에서 state에 들어가야할 내용이 무엇인지 확인하자.

1-2. 복잡한 계산 캐싱하기

  • 아래 컴포넌트는 props로 받은 todosfilter prop에 따라 필터링하여 visibleTodos를 계산한다.
    이 결과를 state 변수에 저장하고 Effect에서 업데이트하고 싶을 수도 있을 것이다.
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // 🔴 중복 state 및 불필요한 Effect 사용을 자제하자.
  const [visibleTodos, setVisibleTodos] = useState([]);
  useEffect(() => {
    setVisibleTodos(getFilteredTodos(todos, filter));
  }, [todos, filter]);

  // ...
}
  • 앞의 예시에서와 마찬가지로 이것은 불필요하고 비효율적이다.
    state와 Effect를 제거하자.
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]);
  // ...
}
  • 이렇게 하면 todosfilter가 변경되지 않는 한 내부 함수가 다시 실행되지 않기를 원한다는 것을 React에 알린다.
    그러면 React는 초기 렌더링 중에 getFilteredTodos()의 반환값을 기억한다.
    그 다음부터는 렌더링 중에 할 일이나 필터가 다른지 확인한다.
    지난번과 동일하다면 useMemo는 마지막으로 저장한 결과를 반환한다.
    같지 않다면, React는 내부 함수를 다시 호출하고 그 결과를 저장한다.
    useMemo로 감싸는 함수는 렌더링 중에 실행되므로, 순수 계산에만 작동한다.

1-3. prop이 변경되면 모든 state 재설정하기

  • 다음 ProfilePage 컴포넌트는 userId prop을 받는다.
    이 페이지에는 코멘트 input이 포함되어 있으며, comment state 변수를 사용하여 그 값을 보관한다.
    어느 날, 한 프로필에서 다른 프로필로 이동할 때 comment state가 재설정되지 않는 문제를 발견했다.
    그 결과 의도치 않게 잘못된 사용자의 프로필에 댓글을 게시하기가 쉬운 상황이다.
    이 문제를 해결하려면 userId가 변경될 때마다 comment state 변수를 지워줘야 한다.
export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');
  // 🔴 prop 변경시 Effect에서 state 재설정 수행하므로 자제
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}
  • 이것은 ProfilePage와 그 자식들이 먼저 오래된 값으로 렌더링한 다음 새로운 값으로 다시 렌더링하기 때문에 비효율적이다.
    또한 ProfilePage 내부에 어떤 state가 있는 모든 컴포넌트에서 이 작업을 수행해야 하므로 복잡하다.
    예를 들어, 댓글 UI가 중첩되어 있는 경우 중첩된 하위 댓글 state들도 모두 지워야 할 것이다.
  • 그 대신 명시적인 키를 전달해 각 사용자의 프로필이 개념적으로 다른 프로필이라는 것을 React에 알릴 수 있다.
    컴포넌트를 둘로 나누고 바깥쪽 컴포넌트에서 안쪽 컴포넌트로 key 속성을 전달하라.
export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ key가 변하면 이 컴포넌트 및 모든 자식 컴포넌트의 state가 자동으로 재설정됨
  const [comment, setComment] = useState('');
  // ...
}
  • 일반적으로 React는 같은 컴포넌트가 같은 위치에서 렌더링될 때 state를 유지한다.
    userIdkeyProfile 컴포넌트에 전달하는 것은 곧, userId가 다른 두 Profile 컴포넌트를 state를 공유하지 않는 별개의 컴포넌트들로 취급하도록 React에게 요청하는 것이다.
    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이 다른 배열을 받을 때마다 selectionnull로 재설정하고 싶다.
function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);
  // 🔴 prop 변경시 Effect에서 state 조정 자제
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}
  • 이것 역시 이상적이지 않다. items가 변경될 때마다 List와 그 하위 컴포넌트는 처음에는 오래된 selection값으로 렌더링된다.
    그런 다음 React는 DOM을 업데이트하고 Effects를 실행한다.
    마지막으로 setSelection(null)호출은 List와 그 자식 컴포넌트를 다시 렌더링하여 이 전체 과정을 재시작하게 된다.
  • Effect를 삭제하는 것으로 시작하라. 대신 렌더링 중에 직접 state를 조정한다.
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);
  }
  // ...
}
  • 이렇게 이전 렌더링의 정보를 저장하는 것은 이해하기 어려울 수 있지만, Effect에서 동일한 state를 업데이트하는 것보다는 낫다.
    위 예시에서는 렌더링 도중 setSelection이 직접 호출된다.
    React는 return문과 함께 종료된 직후에 List를 다시 렌더링한다.
    이 시점에서 React는 아직 List의 자식들을 렌더링하거나 DOM을 업데이트하지 않았기 때문에, List의 자식들은 기존의 selection 값에 대한 렌더링을 건너뛰게 된다.
  • 렌더링 도중 컴포넌트를 업데이트하면, React는 반환된 JSX를 버리고 즉시 렌더링을 다시 시도한다.
    React는 계단식으로 전파되는 매우 느린 재시도를 피하기 위해, 렌더링 중에 동일한 컴포넌트의 state만 업데이트할 수 있도록 허용한다.
    렌더링 도중 다른 컴포넌트의 state를 업데이트하면 오류가 발생한다.
    동일 컴포넌트가 무한으로 리렌더링을 반복 시도하는 상황을 피하기 위해 items !== prevItems와 같은 조건이 필요한 것이다.
    이런 식으로 state를 조정할 수 있긴 하지만, 다른 side effect(DOM 변경이나 timeout 설정 등)은 이벤트 핸들러나 Effect에서만 처리함으로써 컴포넌트의 순수성을 유지해야 한다.
  • 이 패턴은 Effect보다 효율적이지만, 대부분의 컴포넌트에는 필요하지 않다.
    어떻게 하든 props나 다른 state들을 바탕으로 state를 조정하면 데이터 흐름을 이해하고 디버깅하기 어려워질 것이다.
    항상 key로 모든 state를 재설정하거나 렌더링 중에 모두 계산할 수 있는지를 확인하라.
    예를 들어, 선택한 item을 저장(및 재설정)하는 대신, 선택한 item의 ID를 저장할 수 있다.
function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selectedId, setSelectedId] = useState(null);
  // ✅ 가장 좋음: 렌더링 중에 모든 값을 계산
  const selection = items.find(item => item.id === selectedId) ?? null;
  // ...
}
  • 이제 state를 “조정”할 필요가 전혀 없다.
    선택한 ID를 가진 항목이 목록에 있으면 선택된 state로 유지된다.
    그렇지 않은 경우 렌더링 중에 계산된 selection 항목은 일치하는 항목을 찾지 못하므로 null이 된다.
    이 방식은 items에 대한 대부분의 변경과 무관하게 ‘selection’ 항목은 그대로 유지되므로 대체로 더 나은 방법이다.

1-5. 이벤트 핸들러 간 로직 공유

  • 어떤 코드가 Effect에 있어야 하는지 이벤트 핸들러에 있어야 하는지 확실치 않은 경우, 이 코드가 실행되어야 하는 이유를 자문해 보자.
  • 컴포넌트가 사용자에게 표시되었기 때문에 실행되어야 하는 코드에만 Effect를 사용하라.

1-6. POST요청 보내기

  • 어떤 로직을 이벤트 핸들러에 넣을지 Effect에 넣을지 선택할 때, 사용자 관점에서 어떤 종류의 로직인지에 대한 답을 찾아야 한다.
    이 로직이 특정 상호작용으로 인해 발생하는 것이라면 이벤트 핸들러에 넣어라.
    사용자가 화면에서 컴포넌트를 보는 것이 원인이라면 Effect에 넣자.

1-7. 연쇄 계산

  • 이벤트 핸들러에서 직접 다음 state를 계산할 수 없는 경우도 있다.
    예를 들어, 이전 드롭다운의 선택 값에 따라 다음 드롭다운의 옵션이 달라지는 form을 상상해 보자.
    이를 네트워크와 동기화해야 한다면 Effect 체인이 적절할 것이다.

1-8. 애플리케이션 초기화하기

  • 일부 로직은 앱이 로드될 때 한 번만 실행되어야한다.
  • 상용 환경에서는 실제로 다시 마운트되지 않을 수 있지만, 모든 컴포넌트에서 동일한 제약 조건을 따르면 코드를 이동하고 재사용하기가 더 쉬워진다.
    일부 로직이 컴포넌트 마운트당 한 번이 아니라 앱 로드당 한 번 실행되어야 하는 경우, 최상위 변수를 추가하여 이미 실행되었는지 여부를 추적하라.
  • 컴포넌트를 import할 때 최상위 레벨의 코드는 렌더링되지 않더라도 일단 한 번 실행된다.
    임의의 컴포넌트를 임포트할 때 속도 저하나 예상치 못한 동작을 방지하려면 이 패턴을 과도하게 사용하지 말자.

1-9. state변경을 부모 컴포넌트에 알리기

  • 부모 컴포넌트가 더 많은 로직을 포함해야 하지만, 전체적으로 걱정해야 할 state가 줄어든다는 것을 의미힌다.
  • 두 개의 서로 다른 state 변수를 동기화하려고 할 때마다, 대신 state를 끌어올려 보자.

1-10. 부모에게 데이터 전달하기

  • React에서 데이터는 부모 컴포넌트에서 자식 컴포넌트로 흐른다.
    화면에 뭔가 잘못된 것이 보이면, 컴포넌트 체인을 따라 올라가서 어떤 컴포넌트가 잘못된 prop을 전달하거나 잘못된 state를 가지고 있는지 찾아냄으로써 정보의 출처를 추적할 수 있다.
    자식 컴포넌트가 Effect에서 부모 컴포넌트의 state를 업데이트하면, 데이터 흐름을 추적하기가 매우 어려워진다.
    자식과 부모 컴포넌트 모두 동일한 데이터가 필요하므로, 대신 부모 컴포넌트가 해당 데이터를 페치해서 자식에게 전달하도록 하라.

1-11. 외부 스토어 구독하기

  • 때로는 컴포넌트가 React state 외부의 일부 데이터를 구독해야 할 수도 있다.
    서드파티 라이브러리나 브라우저 빌트인 API에서 데이터를 가져와야 할 수도 있다.
    이 데이터는 React가 모르는 사이에 변경될 수도 있는데, 그럴 땐 수동으로 컴포넌트가 해당 데이터를 구독하도록 해야 한다.
    이 작업은 종종 Effect에서 수행한다.
  • React 컴포넌트에서 외부 store를 구독하는 방법에 대해 자세히 읽어보자.

1-12. 데이터 페칭하기

  • 많은 앱이 데이터 페칭을 시작하기 위해 Effect를 사용한다.
  • 데이터 페칭을 구현할 때 경합 조건을 처리하는 것만 어려운 것은 아니다.
    응답을 캐시하는 방법,
    (사용자가 Back을 클릭하고면 스피너 대신 이전 화면을 즉시 볼 수 있도록)
    서버에서 페치하는 방법,
    (초기 서버 렌더링 HTML에 스피너 대신 가져온 콘텐츠가 포함되도록)
    네트워크 워터폴을 피하는 방법
    (데이터를 페치해야 하는 하위 컴포넌트가 시작하기 전에 위의 모든 부모가 데이터 페치를 완료할 때까지 기다릴 필요가 없도록) 등도 고려해볼 사항이다.
  • 이런 문제는 React뿐만 아니라 모든 UI 라이브러리에 적용된다.
    이러한 문제를 해결하는 것은 간단하지 않기 때문에 최신 프레임워크들은 컴포넌트에서 직접 Effect를 작성하는 것보다 더 효율적인 빌트인 데이터 페칭 메커니즘을 제공한다.
  • 프레임워크를 사용하지 않고(또한 직접 만들고 싶지 않고) Effect에서 데이터 페칭을 보다 인체공학적으로 만들고 싶다면, 페칭 로직을 커스텀 훅으로 추출하는 것을 고려해 보자.
  • 또한 오류 처리와 콘텐츠 로딩 여부를 추적하기 위한 로직을 추가하고 싶을 것이다.
    이와 같은 훅을 직접 빌드하거나 React 에코시스템에서 이미 사용 가능한 많은 솔루션 중 하나를 사용할 수 있다.
    이 방법만으로는 프레임워크 빌트인 데이터 페칭 메커니즘을 사용하는 것만큼 효율적이지는 않겠지만, 데이터 페칭 로직을 커스텀 훅으로 옮기면 나중에 효율적인 데이터 페칭 전략을 채택하기가 더 쉬워진다.

요약

  • 렌더링 중에 무언가를 계산할 수 있다면 Effect가 필요하지 않다.
  • 비용이 많이 드는 계산을 캐시하려면 useEffect 대신 useMemo를 추가하라.
  • 전체 컴포넌트 트리의 state를 재설정하려면 다른 key를 전달하자.
  • prop 변경에 대한 응답으로 특정 state 일부를 조정하려면 렌더링 중에 설정하라.
  • 컴포넌트가 표시되었기 때문에 실행해야 하는 코드는 Effect에 있어야 하고, 나머지는 이벤트에 있어야 한다.
  • 여러 컴포넌트의 state를 업데이트해야 하는 경우 단일 이벤트에서 처리하는 것이 좋다.
  • 여러 컴포넌트에서 state 변수를 동기화하려고 할 때마다 state 끌어올리기를 고려하라.
  • Effect로 데이터를 페치할 수 있지만, 경쟁 조건을 피하기 위해 클린업 로직을 구현해야 한다.

React 공식 문서

https://react.dev/

React 비공식 번역 문서

https://react-ko.dev/

MDN

https://developer.mozilla.org/ko/

Wikipedia

https://ko.wikipedia.org/wiki/

profile
함께 일하고 싶어지는 동료, 프론트엔드 개발자입니다.

0개의 댓글