본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -You Might Not Need an Effect(Effect가 필요하지 않을 수도 있습니다)

How to remove unnecessary Effects(불필요한 Effect를 제거하는 방법)

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

1. 렌더링을 위해 데이터를 변환하는 경우 Effect는 필요하지 않다.

2. 사용자 이벤트를 처리하는 데에 Effect는 필요하지 않다.

 

- 외부 시스템과 동기화하려면 Effect가 필요하다


Updating state based on props or state(props 또는 state에 따라 state 업데이트하기)

- 기존 props나 state에서 계산할 수 있는 것이 있으면 state에 넣지 말고, 렌더링 중에 계산하자.

// 잘못된 예시
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 Avoid: redundant state and unnecessary Effect
  // 🔴 이러지 마세요: 중복 state 및 불필요한 Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
  // ...
}

// 올바른 예시
function Form() {
  const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');
  // ✅ Good: calculated during rendering
  // ✅ 좋습니다: 렌더링 과정 중에 계산
  const fullName = firstName + ' ' + lastName;
  // ...
}

Caching expensive calculations(고비용 계산 캐싱하기)

- 느리고 복잡한 계산의 경우 useMemo 훅으로 감싸서 캐시(또는 메모화)할 수 있다.

// useMemo 사용 전
function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  // ✅ This is fine if getFilteredTodos() is not slow.
  // ✅ getFilteredTodos()가 느리지 않다면 괜찮습니다.
  const visibleTodos = getFilteredTodos(todos, filter);
  // ...
}

// getFilteredTodos()가 느리거나, todos가 많은 경우
// useMemo를 사용해서 캐시할 수 있다
import { useMemo, useState } from 'react';

function TodoList({ todos, filter }) {
  const [newTodo, setNewTodo] = useState('');
  const visibleTodos = useMemo(() => {
    // ✅ Does not re-run unless todos or filter change
    // ✅ todos나 filter가 변하지 않는 한 재실행되지 않음
    return getFilteredTodos(todos, filter);
  }, [todos, filter]);
  // ...
}

- useMemo를 사용하면 todos나 filter가 변경되지 않는 한 내부 함수가 다시 실행되지 않기를 원한다는 것을 React에게 알릴 수 있다.

- React는 초기 렌더링 중에 getFilteredTodos()의 반환값을 기억하고, 다음 렌더링부터는 todos나 filter가 지난번과 다른지를 확인하고 동일하다면 useMemo는 마지막으로 저장한 결과를 반환한다.


Resetting all state when a prop changes(prop이 변경되면 모든 state 재설정하기)

- React는 같은 컴포넌트가 같은 위치에 렌더링될 때 state를 공유한다.

- 따라서, 아래 예제에서처럼 userId가 변경될 때, 모든 state를 재설정을 해야한다면, state 설정자 함수를 사용하는 것보다는 key로 Profile 컴포넌트에 전달하게 되며나, userId가 다른 두 Profile 컴포넌트를 state를 공유하지 않는 별개의 컴포넌트들로 취급하게 할 수 있다.

// useEffect와 state 설정자 함수 사용
export default function ProfilePage({ userId }) {
  const [comment, setComment] = useState('');

  // 🔴 Avoid: Resetting state on prop change in an Effect
  // 🔴 이러지 마세요: prop 변경시 Effect에서 state 재설정 수행
  useEffect(() => {
    setComment('');
  }, [userId]);
  // ...
}


// key 사용
export default function ProfilePage({ userId }) {
  return (
    <Profile
      userId={userId}
      key={userId}
    />
  );
}

function Profile({ userId }) {
  // ✅ This and any other state below will reset on key change automatically
  // ✅ key가 변하면 이 컴포넌트 및 모든 자식 컴포넌트의 state가 자동으로 재설정됨
  const [comment, setComment] = useState('');
  // ...
}

Adjusting some state when a prop changes(props가 변경될 때 일부 state 조정하기)

- props의 일부가 변경될 때, state의 전체가 아닌 일부만 재설정하거나 조정하고 싶을 수 있다.

- 이런 경우, 렌더링 중에 직접 state를 조정하는 방법이 있다.

// 지양
function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // 🔴 Avoid: Adjusting state on prop change in an Effect
  // 🔴 이러지 마세요: prop 변경시 Effect에서 state 조정
  useEffect(() => {
    setSelection(null);
  }, [items]);
  // ...
}

// 지향
function List({ items }) {
  const [isReverse, setIsReverse] = useState(false);
  const [selection, setSelection] = useState(null);

  // Better: Adjust the state while rendering
  // 더 나음: 렌더링 중에 state 조정
  const [prevItems, setPrevItems] = useState(items);
  if (items !== prevItems) {
    setPrevItems(items);
    setSelection(null);
  }
  // ...
}

- 위 예제에서는 이전 렌더링의 정보를 저장하고 있고, 렌더링 도중 setSelection이 호출되고 있다.

- React는 return 문과 함께 종료된 직후에 List를 다시 렌더링하는데, 이 시점에서 React는 아직 List의 자식들을 렌더링하거나 DOM을 업데이트하지 않았기 때문에, List의 자식들은 기존의 selection 값에 대한 렌더링을 건너뛰게 된다.(?)

 

- 렌더링 도중 컴포넌트를 업데이트하면, React는 반환된 JSX를 버리고 즉시 렌더링을 다시 시도한다.

- React는 계단식으로 전파되는 매우 느린 재시도를 피하기 위해, 렌더링 중에 동일한 컴포넌트의 state만 업데이트할 수 있도록 허용한다.

 

- 하지만, props나 다른 state들을 바탕으로 state를 조정하면 데이터 흐름을 이해하고 디버깅하기 어려울 수 있다.

- 따라서, key로 모든 state를 재설정하거나 렌더링 중에 모두 계산할 수 있는지 확인하는 것이 중요하다.


Sharing logic between event handlers(이벤트 핸들러 간 로직 공유)

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


Sending a POST request(POST 요청 보내기)

- 어떤 로직이 특정 상호작용으로 인해 발생하는 것이라면 Effect가 아닌 이벤트 핸들러에 넣자.


Chains of computations(연쇄 계산)

- 다른 state를 바탕으로 또다른 state를 조정하는 Effect를 연쇄적으로 사용한다면, 아래와 같은 문제가 발생할 수 있다.

1. 매우 비효율적이다 : 컴포넌트(및 그 자식들)은 체인의 각 state setter 함수 호출 사이에 다시 렌더링 해야한다. 즉, 불필요한 렌더링이 많이 발생할 수 있다.

2. 체인이 새로운 요구사항에 맞지 않는 경우가 발생할 수 있다 : 아래 예제에서 "card"의 state를 과거의 값으로 설정하면 다시 Effect 체인이 촉발되고 표시되는 데이터가 변경될 수 있다.

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);
  const [isGameOver, setIsGameOver] = useState(false);

  // 🔴 Avoid: Chains of Effects that adjust the state solely to trigger each other
  // 🔴 이러지 마세요: 오직 서로를 촉발하기 위해서만 state를 조정하는 Effect 체인
  useEffect(() => {
    if (card !== null && card.gold) {
      setGoldCardCount(c => c + 1);
    }
  }, [card]);

  useEffect(() => {
    if (goldCardCount > 3) {
      setRound(r => r + 1)
      setGoldCardCount(0);
    }
  }, [goldCardCount]);

  useEffect(() => {
    if (round > 5) {
      setIsGameOver(true);
    }
  }, [round]);

  useEffect(() => {
    alert('Good game!');
  }, [isGameOver]);

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    } else {
      setCard(nextCard);
    }
  }

  // ...

- 위 예제의 해결방법은 렌더링 중에 가능한 것을 계산하고 이벤트 핸들러에서 state를 조정하는 것이다.

function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ Calculate what you can during rendering
  // ✅ 가능한 것을 렌더링 중에 계산
  const isGameOver = round > 5;

  function handlePlaceCard(nextCard) {
    if (isGameOver) {
      throw Error('Game already ended.');
    }

    // ✅ Calculate all the next state in the event handler
    // ✅ 이벤트 핸들러에서 다음 state를 모두 계산
    setCard(nextCard);
    if (nextCard.gold) {
      if (goldCardCount <= 3) {
        setGoldCardCount(goldCardCount + 1);
      } else {
        setGoldCardCount(0);
        setRound(round + 1);
        if (round === 5) {
          alert('Good game!');
        }
      }
    }
  }

  // ...

- 여러 이벤트 핸들러 간에 로직을 재사용해야 하는 경우 함수를 추출하여 해당 핸들러 함수에서 호출할 수 있다.

- 이벤트 핸들러 내부에서 state는 스냅샷처럼 동작함을 기억해라.

- 하지만, 이벤트 핸들러에서 직접 다음 state를 계산할 수 없는 경우(네트워크와 동기화 해야하는 경우)에는 Effect 체인이 적절할 수 있다.


Initializing the application(애플리케이션 초기화하기)

function App() {
  // 🔴 Avoid: Effects with logic that should only ever run once
  // 🔴 이러지 마세요: 한 번만 실행되어야 하는 로직이 포함된 Effect
  useEffect(() => {
    loadDataFromLocalStorage();
    checkAuthToken();
  }, []);
  // ...
}

// 위 함수는 개발 중에 두 번 실행되게 되고, 인증 토큰이 무효화되는 등의 문제가 발생할 수 있다.

// 해결방법 1
// 제약 조건을 이용하기

let didInit = false;

function App() {
  useEffect(() => {
    if (!didInit) {
      didInit = true;
      // ✅ Only runs once per app load
      // ✅ 앱 로드당 한 번만 실행됨
      loadDataFromLocalStorage();
      checkAuthToken();
    }
  }, []);
  // ...
}

// 해결방법 2
// 모듈 초기화 중이니 앱 렌더링 전에 실행
if (typeof window !== 'undefined') { // Check if we're running in the browser.
                                     // 브라우저에서 실행중인지 확인
  // ✅ Only runs once per app load
  // ✅ 앱 로드당 한 번만 실행됨
  checkAuthToken();
  loadDataFromLocalStorage();
}

function App() {
  // ...
}

- 컴포넌트를 import할 때 최상위 레벨의 코드는 렌더링되지 않더라도 일단 한 번 실행되기 때문에, 임의의 컴포넌트를 임포트할 때 속도 저하나 예상치 못한 동작을 방지하려면 이 패턴을 과도하게 사용하면 안된다.

- 앱 전체 초기화 로직은 App.js와 같은 루트 컴포넌트 모듈이나 애플리케이션의 엔트리 포인트에 유지하자.


Notifying parent components about state changes(state 변경을 부모 컴포넌트에 알리기)

- 부모 컴포넌트에게 state setter 함수를 prop으로 전달받고 useEffect 내부에서 실행하게 되면, 자식 컴포넌트의 state가 변경되어 업데이트를 실행하여 화면을 업데이트하고, 그런 다음 부모 컴포넌트로부터 전달받은 state setter 함수를 실행하기 때문에, 이런 로직은 단일 명령 안에서 처리하는 것이 좋다.

// 잘못된 예시
function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  // 🔴 Avoid: The onChange handler runs too late
  // 🔴 이러지 마세요: onChange 핸들러가 너무 늦게 실행됨
  useEffect(() => {
    onChange(isOn);
  }, [isOn, onChange])

  function handleClick() {
    setIsOn(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      setIsOn(true);
    } else {
      setIsOn(false);
    }
  }

  // ...
}

// 올바른 예시
function Toggle({ onChange }) {
  const [isOn, setIsOn] = useState(false);

  function updateToggle(nextIsOn) {
    // ✅ Good: Perform all updates during the event that caused them
    // ✅ 좋습니다: 이벤트 발생시 모든 업데이트를 수행
    setIsOn(nextIsOn);
    onChange(nextIsOn);
  }

  function handleClick() {
    updateToggle(!isOn);
  }

  function handleDragEnd(e) {
    if (isCloserToRightEdge(e)) {
      updateToggle(true);
    } else {
      updateToggle(false);
    }
  }

  // ...
}

- 올바른 예시와 같이 로직을 작성하면, 자식 컴포넌트와 부모 컴포넌트 모두 이벤트가 발생하는 동안 state를 업데이트 하고, React는 서로 다른 컴포넌트에서 일괄 업데이트를 함께 처리하므로, 렌더링 과정은 한 번만 발생한다.

-  자식 컴포넌트에서 state를 완전히 제거하고 부모 컴포넌트로부터 state와 state setter 함수를 prop으로 받을 수도 있다.


Passing data to the parent(부모에게 데이터 전달하기)

- 자식 컴포넌트가 Effect에서 부모 컴포넌트의 state를 업데이트하면, 데이터 흐름을 추적하기가 매우 어려워진다.

- 자식 컴포넌트와 부모 컴포넌트 모두 동일한 데이터가 필요하므로, 대신 부모 컴포넌트가 해당 데이터를 패치해서 자식에게 전달하도록 하자.


Subscribing to an external store(외부 스토어 구독하기)

- 서드 파티 라이브러리나 브라우저 빌트인 API에서 데이터를 가져오는 경우, React가 모르는 사이에 변경될 수도 있는데, 이럴 때는 수동으로 Effect를 사용하여 해당 데이터를 구독하게 할 수 있다.


Fetching Data(데이터 페칭하기)

- 경쟁 조건을 수정하기 위해서는 오래된 응답을 무시하도록 클린업 함수를 추가해야 한다.

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);
  useEffect(() => {
    let ignore = false;
    fetchResults(query, page).then(json => {
      if (!ignore) {
        setResults(json);
      }
    });
    return () => {
      ignore = true;
    };
  }, [query, page]);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

- 위 예제처럼 작성하면, Effect가 데이터를 페치할 때 마지막으로 요청된 응답을 제외한 모든 응답이 무시된다.

- cleanup 함수 뿐만 아니라, 응답을 캐시하는 방법, 서버에서 페치하는 방법, 네트워크 워터폴을 피하는 방법 등도 고려해볼 사항이다.

- 최신 프레임워크들은 컴포넌트에서 직접 Effect를 작성하는 것보다 더 효율적인 빌트인 데이터 페칭 메커니즘을 제공한다.

- 또는, 아래와 같이 프레임워크를 사용하지 않고(또한 직접 만들지 않고) Effect에서 데이터 페칭을 보다 인체공학적(?)으로 하고 싶다면, 패칭 로직을 커스텀 훅으로 추출하는 것을 고려해 볼수 있다.

function SearchResults({ query }) {
  const [page, setPage] = useState(1);
  const params = new URLSearchParams({ query, page });
  const results = useData(`/api/search?${params}`);

  function handleNextPageClick() {
    setPage(page + 1);
  }
  // ...
}

function useData(url) {
  const [data, setData] = useState(null);
  useEffect(() => {
    let ignore = false;
    fetch(url)
      .then(response => response.json())
      .then(json => {
        if (!ignore) {
          setData(json);
        }
      });
    return () => {
      ignore = true;
    };
  }, [url]);
  return data;
}

* 참고 : React 공식문서(https://react-ko.dev/learn)