useEffect가 필요하지 않을 수도 있습니다.

jade·2025년 2월 6일
0

REACT 공식문서

이런 상황에서는 Effect가 필요합니다.

외부 시스템과 “동기화”해야할 때

주로 React 코드를 벗어난 특정 외부 시스템과 동기화 하기 위해 사용된다. 여기서 외부 시스템은 브라우저 API, 서드파티위젯, 네트워크 등이 있다.

예) useEffect로 데이터를 가져오기

예 ) 검색결과를 현재 검색 쿼리와 동기화기

Next.js Gatsby, Remix와 같은 프레임워크는 컴포너트에 직점 UseEffect를 작성하는 것보다 더 효율적인 데이터 가져오기 매커니즘이 존재한다.😉


useEffect가 필요하지 않을 수도 있습니다 : 불필요한 Effect를 제거하기

: 요런 상황에서 Effect 사용을 지양해 보세요😉

  • 단순히 다른상태에 기반해서 일부 상태를 조정할 때

렌더링을 위해 데이터를 변화할때

예 ) 리스트데이터를 화면에 뿌려주기 전에 필터링 할때

(현재 프로젝트에서는 사용자의 모든 plan을 가져와서 이중 특정 날짜에 해당하는 plan만 필터링하는 로직이 필요하다.)

리스트가 변경될 때 state변수를 없데이트 하는 useEffect를 작성하려는 충동이 들수 있는데 이는 비효율적이다.

이렇게 구현하면 다음과 같은 과정을 거치는데

  1. 함수 컴포넌트를 호출해서 화면에 표시될 내용을 계산한다.

  2. DOM에 변경사항을 커밋한다.(Commit phase)

    • 초기 랜더링의 경우 생성한 모든 DOM노드를 화면에 표시한다.
    • 리랜더링의 경우 최소한의 작업(렌더링하는 동안 계산되어 변경된 것)을 DOM에 적용한다.

    (https://ko.react.dev/learn/render-and-commit)

  3. Effect를 실행한다. → 이때 내부에서 state를 변경하면 위의 과정이 다시 실행된다.

따라서 불필요한 랜더링 패스를 피하려면 컴포넌트의 최상위 레벨에서 모든 데이터를 변환해야 한다.

사용자 이벤트를 처리할 때

예 ) 사용자가 제품을 구매하기 위해 post요청을 전송하고 알림을 표시하고 싶을 때

(현재 프로젝트에서는 plan데이터를 제출하고 onSuccess일 경우 ‘계획이 추가되었습니다!’라는 토스트 알림을 띄운다)

이벤트 핸들러에서 사용자 이벤트를 처리하자

why ) useEffect에서 처리하게 되면 effect가 실행될 때가지 사용자가 무엇을 했는지 ( ex- 어떤 버튼을 눌렀는지 )알 수 없다. 구매버튼 클릭 이벤트 핸들러에서는 정확기 어떤 일이 벌어졌는지 알 수 있다.

구체적인 예시로 다지기

1. 상태에서 파생된 데이터를 또 다시 상태에 넣지 말자

  • 원천에서 파생된 데이터는 ‘상태’가 아니다!
  • 대신 랜더링 중에 계산하게 만들자

📌 잠깐 멈추고 생각해보세요!
사용자가 성, 이름을 입력 할 때마다 이를 합친 fullName이라는 변수가 있다고 생각해봅시다.

여기서 fullName은 상태로 다뤄져서는 안된다. 즉, useEffect의 의존성배열에 성, 이름을 넣어두고 이들이 바뀔 때바다 setFullname을 호출하는 Effect를 만들어선 안된다.

 const [firstName, setFirstName] = useState('Taylor');
  const [lastName, setLastName] = useState('Swift');

  // 🔴 피하세요: 중복된 state 및 불필요한 Effect
  const [fullName, setFullName] = useState('');
  useEffect(() => {
    setFullName(firstName + ' ' + lastName);
  }, [firstName, lastName]);
// ✅ 좋습니다: 렌더링 중에 계산됨
  const fullName = firstName + ' ' + lastName;

2. 비용이 많이 드는 계산은 캐싱합시다.

사용자의 모든 plan을 가져와서 이중 특정 날짜에 해당하는 plan만 필터링해서 보여주는 기능을 만들어 봅시다.

function PlanList({ planss, filter }) {
  const [newPlans, setNewPlans] = useState('');

  // 🔴 피하세요: 중복된 state 및 불필요한 효과
  const [visiblePlans, setVisiblePlans] = useState([]);
  useEffect(() => {
    setVisiblePlans(getFilteredPlans(plans, filter));
  }, [plans, filter]);

  // ...
}
 // ✅ getFilteredPlans()가 느리지 않다면 괜찮습니다.
  const visiblePlans = getFilteredPlans(plans, filter);

보통은 괜찮지만, getFilteredPlans이 비효율적인 필터링 알고리즘을 가지거나 plans의 양이 많아지면 비용이 해당 계산의 비용이 아주아주 비싸질 수 있다.

const visiblePlans = useMemo(() => getFilteredPlans(plans, filter)},[plans,filter]);

이럴때는 useMemo를 감싸서 두번째 랜더링시에는 (todos, filter가 변하지 않는다면 )계산된 값을 사용하도록 할 수 있다.

🥰 gerFilteredPlans함수가 자체적으로 “날짜”를 필터링하는 로직을 가지고 있기 보다는 “날짜”라는 필터를 외부에서 주입받아 좀더 범용성 있는 함수로 만드는 방법이다! 이렇게 되면 카테고리별로 필터링할수도 잇고,, 기존에 짠 코드보다 더 좋은 방법같다.

3. useEffect 체이닝 🙅

하나의 상태가 변경되면 또 다른 상태를 변경하고 싶은 마음이 들 수 있다.

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

  // 🔴 피하세요: 서로를 트리거하기 위해서만 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);
    }
  }

이 코드에는 두가지 문제가 있는데

  1. 매우 비효율적이다.
  • 최악의 경우 총 4번 랜더링이 트리거 되며 총 3번의 불필요한 리랜더링이 발생한다. IMG_0482.jpg
  1. 속도가 느리지 않아도 코드가 발전함에 따라 작성한 체인이 새로운 요구사항에 맞지 않는 경우가 생길 수 있다.
function Game() {
  const [card, setCard] = useState(null);
  const [goldCardCount, setGoldCardCount] = useState(0);
  const [round, setRound] = useState(1);

  // ✅ 렌더링 중에 가능한 것을 계산합니다.
  const isGameOver = round > 5;

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

    // ✅ 이벤트 핸들러에서 다음 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는 스냅샷처럼 동작합니다.
    • 계산한 다음 값을 사용해야 한다면 const nextRound = round +1 처럼 수동으로 정의

🧐 그러나 직접 다음 state를 계산할 수 없는 경우도 있다.

ex) 여러 드롭다운이 존재하는 폼에서 다음 드롭 다운 옵션이 이전 드롭다운의 선택에 따라 좌우될 때

→ 네트워크와 동기화 해야하므로 useEffect 체인이 적절

4. 어떤 로직이 Effect내에서 실행되어야하는지 아닌지 모를때는 <이 코드가 실행되어야 하는 이유>를 생각해보자.

  • 해당 컴포넌트가 “표시되었기 때문에”처리해야하는 로직은 useEffect에 둔다. 예시 ) 해당 컴포넌트가 표시되었을 때 analytics POST 요청이 가야 한다. → Effect에 있어야 한다. analystics 이벤트가 전송되어야 하는 이유는 폼이 사용자에게 표시되었기 때문이다.
  • 특정 상호작용에서만 트리거 되어 수행되어야하는 로직은 이벤트 핸들러로 이동한다.
    • 여러 이벤트 핸들러에서 공유되는 로직은 함수에 넣어 핸들러 내부에서 호출한다.

5. 두개의 state변수를 동기화 해야할 때

업데이트를 유발한 이벤트가 모든 업데이트를 수행하자

하나의 이벤트로 인해 두 컴포넌트의 상태가 업데이트 되어야 하는경우

(useEffect로 하나의 상태가 변경되었을 때 → effect 내부에서 다른 상태를 업데이트하지 말고!)

해당 이벤트 핸들러 내에서 setState를 모두 호출해서 상태 업데이트를 일괄로 처리하자 → 랜더링 1번만!

“상태 끌어올림”으로 제어권을 부모에게 완전히 넘긴다.

→ 이렇게 하면 부모컴포넌트는 더많은 로직을 포함해야 하지만 전체적으로 신경써야할 state 수는 줄어든다.

6. 데이터 가져오기

데이터를 가져오기 위해 useEffect를 사용하는 것은 일반적인 방법이다.

이제 아래 케이스를 확인해 보자.

input창에서 검색어를 입력하면 이를 query에 담아서 , query에 대한 검색 결과를 보여주는 컴포넌트가 있다고 하자.

function SearchResults({ query }) {
  const [results, setResults] = useState([]);
  const [page, setPage] = useState(1);

  useEffect(() => {
    // 🔴 피하세요
    fetchResults(query, page).then(json => {
      setResults(json);
    });
  }, [query, page]);

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

위 코드에서 useEffect가 사용되는 목적은 page, query에 대한 서버데이터와 results를 동기화하고자 함이다.

🤚 위 코드에는 문제가 있습니다. 잠깐 멈추고 생각해 봅시다!

(생각했나요…? 가봅시다. )

input 창에서 ‘hello’를 빠르게 입력한다고 해봅시다. 그러면 h → he → hel → hell → hello로 바뀌면서 바뀔 때마다 fetchResult를 호출하여 데이터를 가져오기 시작하지만 문제는 응답이 어떤 순서로 올지 보장할 수 업습니다.

그러니까 hello응답이 도착한 뒤, hel의 응답이 올수도 있죠.

그리고 setResult()를 마지막으로 호출하므로 잘못된 결과가 표시될 수 있습니다. 이를 “경쟁조건”이라고 합니다.

서로 다른 요청이 “경쟁”하여 예상과 다른 순서로 도착하는 것을 말합니다.

이런 경쟁조건을 수정하려면 이전 응답을 무시하는 정리함수 를 추가해야 합니다.

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가 데이터를 가져올 때 마지막으로 요청된 응답을 제외한 모든 응답이 무시됩니다.

경쟁 상태 외에도 데이터 패칭시에 고려해야할 점들은 여전히 많습니다.

  • 응답 캐싱 : 사용자가 뒤로가기 버튼을 클릭하여 이전 화면을 볼 수 있도록 ⇒ 이번 프로젝트에서도 여행일정등록과정에서 검색 결과 → 상세보기 → 다시 검색 결과로 돌아올때 해당 상황을 고려해야했습니다.
  • 서버에서 데이터를 가져오는 방법 : 초기 서버 랜더링 HTML에 스피너 대신 가져온 콘텐츠가 포함되도록
  • 네트워크 워터폴을 피하는 방법 : 자식이 모든 부모를 기다리지 않고 데이터를 가져올 수 있도록

데이터 패칭로직을 커스텀훅으로 추출하기

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;
}
  • 컴포넌트에서 원시적인 useEffect 호출이 적을수록 애플리케이션을 유지보수 하기가 더 쉬워집니다.

props 변경시 모든 state를 초기화하려면

lee 에게서 달린 댓글을 보다가 kim을 클릭할 경우 댓글 목록은 리셋되어야 한다.

그런데 위처럼 userEffect를 사용해서 상태를 초기화 시킬경우 userIndex가 변경되었음에도 불구하고

이전 lee의 코멘트가 달린 화면을 다시 랜더링한다 → comment를 초기화 한다. → 초기화된 화면을 다시 랜더링한다.

  • 만약 초기화해야할 값이 Comment 뿐이 아니라면?
  • Profile의 자식 컴포넌트에서도 State을 다룬다면?

lee의 프로필을 보며 댓글을 달다가 kim의 프로필을 보려 버튼을 누르게 되면,
Profile의 comment는 초기화 되었지만 자식인 ProfileChild 컴포넌트의 상태는 False로 초기화 되지 않고 여전히 true이다.

⇒ 즉 Profile내부에서 중첩된 모든 컴포넌트에서 초기화 작업을 진행해야 하므로 복잡성이 증가한다.

명시적인 key를 전달하여 리액트에게 프로필이 개념적으로 “다른” 프로필임을 알려주기

  • 일반적으로 React는 동일한 컴포넌트가 같은 위치에 렌더링 될 때 state를 보존합니다. Profile 컴포넌트에 userIdkey로 전달하면 React가 userId가 다른 두 개의 Profile 컴포넌트를 state를 공유해서는 안 되는 두 개의 다른 컴포넌트로 취급하도록 요청하는 것입니다.
  • userId로 설정한 key가 변경될 때마다 React는 DOM을 다시 생성하고 Profile 컴포넌트와 그 모든 자식의 state를 재설정합니다.
  • 즉 부모의 key가 다르면 DOM을 재생성 할 뿐 아니라 자식 컴포넌트내의 모든 내부 state도 초기화 한다.
<div className="App">
      <Side onClick={onClick} />
      <Profile userIndex={userIndex} key={users[userIndex].userId} />
 </div>

따라서 이제 useEffect를 사용하여 초기화하는 코드를 제거하여도, 자동으로 모든 state들이 초기화된다.

https://codesandbox.io/p/sandbox/htjjds?file=%2Fsrc%2FApp.js%3A4%2C1

profile
keep on pushing

0개의 댓글

관련 채용 정보