useEffect에서 비동기 로직 처리하기

jimmy neutron·2022년 9월 14일
1

React

목록 보기
2/3

문제 발생

일상공유 sns인 dailyLife를 만드는 프로젝트를 진행하던 중 게시물 컴포넌트가 불필요하게 리렌더링이 일어나는 문제가 생겼다.

상태가 변한것도 아닌데 각 컴포넌트별로 최소 5번 이상 불필요한 리렌더링이 발생하니 문제가 심각하다고 생각되어 오류를 고치기위해 구글링을 해보았다. 이번 글에는 내가 불필요한 리렌더링을 막는 과정을 담았다. 같이 떠나보자~

1. 문제 파악

리렌더링 확인을 위해 찍어놓은 콘솔이 400회 정도 실행되고 있다.

리렌더링이 되는 게시물 컴포넌트는 예전에 프로젝트에 일주일 정도 참여하셨던 분이 작성하고 간 코드인데 별 문제가 없어서 리팩토링을 미루고 있었다.
그런데 관련 로직을 리팩토링하다가 리렌더링 문제를 발견해서 이번 기회에 천천히 살펴보았다.

1-1. 의심가는 코드 확인

우선 콘솔을 찍어본 결과 리액트의 reder phase까지는 문제가 없어보였다. 그렇다면 commit phase에서 문제가 발생했다는 건데 가장 확률이 높은 오류 코드는 commit phase의 핵심인 side effect를 발생시키는 useEffect훅일거라는 생각이 들었다.

useEffect(() => {
    axios
      .get( `${process.env.REACT_APP_HOST}/api/board/getBoard`,
        {
          headers: {
            'X-ACCESS-TOKEN':
              localStorage.getItem('accessToken'),
          },
        },
      )
      .then((res) => {
        dispatch(
          postActions.updateItems(res.data),
        );
      })
      .catch((res) => {
        console.log(res);
      });
  }, []);

역시 비동기 로직을 useEffect에서 그대로 사용하고 있었다.
axios공식 문서에서는 axios를 이렇게 정의한다.

Axios is a promise-based HTTP Client for node.js and the browser.

즉 axios는 promise객체를 이용해 비동기를 처리한다는 것인데 promise객체만을 사용했을 때의 문제점이 여기서 드러난다.

1-2. Promise(axios)객체로 비동기 처리시 문제점

서버에서 api로 데이터를 받아왔다고 가정해보자. Promise객체만을 사용해서 데이터를 받아왔기 때문에 데이터를 받아온 시점에 콜백 함수.then()등을 이용해서 후속 처리를 해주어야한다. 즉, 그 외에의 코드는 데이터를 받아오지 않은 시점에도 실행이 되버린다는 것이다. 이를 방지하기 위해 .then()지옥이 펼쳐지는데 이것을 해결해준것이 바로 async/await이다.

2. 문제 해결 과정

2-1. async/await 사용하기

앞에 말했듯 async/await을 사용하면 본래 promise만을 사용했을 때의 문제점을 해결 할 수 있는데 async함수 내에서 await가 붙은 비동기 처리 코드가 응답이 올 때까지 기다려준다는 점을 이용한다.

예를 들어보자.

function fetchItems() {
    return new Promise((resolve, reject) => {
    const items = [1, 2, 3];
    resolve(items);
  });
}
    
async function logItemsWithAsync() {
  const resultItems = await fetchItems();
  console.log(resultItems);
  consol.log('mission accomplished!');
}

// 출력 결과
// [1, 2, 3]
// mission accomplished!


function logItemWithoutAsync() {
  fetchItems().then((res) => console.log(res));
  console.log('mission accomplished!');
}

// 출력 결과
// mission accomplished!
// [1, 2, 3]

위의 코드에서 알 수 있듯이 async/await을 사용하면 비동기에 대한 사고를 하지 않아도 된다.

이제 문제의 코드를 async/await을 이용해 바꿔보자.

useEffect(async() => {
  const items = await axios.get(
      `${process.env.REACT_APP_HOST}/api/board/getBoard`,
      {
        headers: {
          'X-ACCESS-TOKEN': localStorage.getItem(
            'accessToken',
          ),
        },
      },
    );
    const itemsData = await items.then(
        (res) => res.data,
      );
   dispatch(
        postActions.updateItems(itemsData),
      );
  }, []);

리렌더링 확인을 위해 찍어놓은 콘솔이 약 400회에서 81회로 확연하게 줄었다.
콘솔을 확인해보니 render phase에서 commit phase까지 정확하게 실행되는것을 확인할 수 있었다.

3. 또 다른 에러 발생

3-1. useEffect 반환값 에러 발생

리렌더링은 잡았지만 다른 에러가 발생했다.

useEffect hook은 아무것도 반환하지 않거나 clean-up함수를 반환한다는 내용인데 아마도 async 키워드를 사용하면 promise객체를 반환하므로 에러가 발생하는것 같다.

async와 useEffect

우선 에러를 해결하기 위해 useEffect의 구조부터 알아보자.

useEffect(
  // 이펙트 함수
  () => {
    
  // 클린업 동작
    return () => {
    }
  },[의존값 목록])

내부의 이펙트 함수는 컴포넌트의 첫 렌더링과 의존값 목록의 값이 변하면 호출되고, 이펙트 함수에서 반환하는 함수는 이펙트 함수가 호출되기 전과 언마운트될 때 한 번씩 호출된다. 이 때 반환해야하는 함수가 clean-up함수이다. clean-up함수는 useEffect의 뒷정리를 해준다고 생각하면 된다.

만약 이펙트 함수가 async함수라면 어떻게 될까?

// async 함수를 이펙트로 전달
useEffect(async function, []);

// 이펙트 함수의 리턴값

return new Promise();

발생하는 문제

async함수를 그대로 이펙트 함수로 전달하면 구조상 promise객체를 리턴하게 되고, 이펙트 함수에서는 클린업 함수를 리턴해야 하는데 이미 promise를 리턴하고 함수가 종료되므로 리액트가 받아들이는건 달랑 promise객체 하나이다.

이로 인해 렌더링 성능에도 영향을 미칠 수 있고(클린업이 안되므로) 경우에 따라서는 다른 컴포넌트를 렌더링 할 시 아예 오류를 출력하기도 한다.

3-1-1. 해결책

개발자 도구의 경고문에 해결책이 적혀있다.

useEffect(() => {
    async function fetchItemData() {
      const items = await axios.get(
        `${process.env.REACT_APP_HOST}/api/board/getBoard`,
        {
          headers: {
            'X-ACCESS-TOKEN':
              localStorage.getItem('accessToken'),
          },
        },
      );
      const itemsData = await items.then(
        (res) => res.data,
      );
      dispatch(
        postActions.updateItems(itemsData),
      );
    }
    fetchItemData();
  }, [like]);

이렇게 이펙트 함수 내부에서 async함수를 선언해주고 바로 async함수를 호출하는것인데 이렇게 하면 async함수가 promise를 반환하더라도 이펙트는 아무것도 반환하지 않아 클린업이 제대로 작동하게 된다.

참고 자료

https://merrily-code.tistory.com/117
https://developer-talk.tistory.com/250
https://joshua1988.github.io/web-development/javascript/js-async-await/
https://velog.io/@bey1548/axios-await-async

profile
프론트엔드로 지구정복

0개의 댓글