React를 다루는 기술 14 - 비동기 방식의 HTTP 통신

3 minute read

비동기 작업의 이해

웹 어플리케이션을 만들다보면 처리할 때 까지 시간이 걸리는 작업이 있다.
예를 들어 어플리케이션에서 서버 쪽의 데이터가 필요할 경우 Ajax를 통해서 서버의 API를 호출함으로써 데이터를 수신하는데 이 과정에서 네트워크 송수신 과정에 걸리는 시간이 존재하기 때문에 작업시간이 소요된다.
때문에 이 작업이 완료될 때 까지는 다른 작업이 실행되지 않으며 끝날때까지 기다려야하는 상황이 되는데 이것이 동기적 작업이다.

비동기적 작업은 이와 달리 여러개의 요청이 한꺼번에 들어와도 동시에 시작하여 개별적으로 처리하는 작업을 뜻한다.
그래서 작업 도중에 다른 작업을 호출하거나 동시에 여러가지의 요청을 처리할 수 있다.

이렇게 서버 API를 호출 할 때 외에도 setTimeout 함수를 사용하여 특정작업을 예약하는 것 역시 비동기적 작업의 하나이다.

function printMe() {
  console.log('Hello world');
}

setTimeout(printMe, 3000);
console.log('대기 중...');

// 결과
// 대기 중...
// Hello world

setTimeout을 사용한 시점에서 함수가 멈추는 것이 아니라 브라우저 상에 등록되어 3초 후에 실행되며 그 과정에서 들어온 다른 명령을 기다리지 않고 바로바로 처리하는 모습을 볼 수 있다.
이렇게 함수(setTimeout) 안에서 또 다른 함수를 호출하는 것을 콜백함수라고 하며 자바스크립트에서 비동기적 작업을 하기 위한 가장 기본적인 형태이다.

Promise

그런데 콜백함수가 1개에 그치지 않고 여러차례의 작업이 연속되어야하는 경우 콜백함수 안에 새로운 콜백함수가 들어가는 구조가 반복되어 가독성이 아주 나쁜 콜백지옥이 이루어진다.
Promise는 이를 해결하기 위해 ES6부터 도입된 기능이다.

function increase(number) {
  const promise = new Promise((resolve, reject) => {
    // resolve = 성공, reject = 실패
    setTimeout(() => {
      const result = number + 10;
      if (result > 50) {
        const e = new Error('NumberTooBig');
        return reject(e); // 실패
      }

      resolve(result); // 성공
    }, 1000);
  });

  return promise;
}

increase(0)
  .then(number => {
    console.log(number);
    return increase(number);
  })
  .then(number => {
    console.log(number);
    return increase(number);
  })

Promise를 생성하여 성공 시 반환해주면 .then을 통해서 resolve 함수에 넣어준 파라미터를 받아 작업을 진행할 수 있다.
그리고 다시 함수를 반환하고 .then을 이용하는 것을 통해 콜백함수를 이어나갈 수 있다.

async/await

async/await는 위의 Promise를 좀 더 쉽게 사용할 수 있도록 추가된 ES2017(ES8) 문법이다.
함수의 앞부분에 async를 추가하고 함수 내부의 Promise 앞부분에 await를 추가해주면 .then을 통해 다시 반환하는 과정일 이어나가지 않고도 여러번 콜백함수를 이어나갈 수 있다.

function increase(number) {
  const promise = new Promise((resolve, reject) => {
    // resolve = 성공, reject = 실패
    setTimeout(() => {
      const result = number + 10;
      if (result > 50) {
        const e = new Error('NumberTooBig');
        return reject(e); // 실패
      }

      resolve(result); // 성공
    }, 1000);
  });

  return promise;
}

async function runTasks() {
  try{
    let result = await increase(0);
    console.log(result);

    result = await increase(result);
    console.log(result);

    result = await increase(result);
    console.log(result);

    result = await increase(result);
    console.log(result);
  } catch(e) {
    console.log(e);
  }
}

axios

axios는 현재 가장 많이 사용되고 있는 HTTP 클라이언트로 위에서 설명한 Promise 기반으로 HTTP 요청을 처리한다는 점이 특징이다.

import React, { useState, useEffect } from 'react';
import axios from 'axios';

const App = () => {
  const [ datas, setDatas ] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await axios.get('통신할 url',);
        setDatas(response.data);
      } catch (e) {
        console.log(e);
      }
    };

    fetchData();
  }, []);

  if(!datas) {
    return null;
  }

  return (
    <ul>
      {
        datas.map((data) => { 
          return (
            <li>{data}</li>
          )
        })
      }
    </ul>
  )
}

fetchData 함수의 axios를 통해 get 방식으로 서버API와 통신하여 결과를 받아 해당 결과값을 data에 넣어주고 표시하는 컴포넌트이다.
여기서 중요한 것은 useEffect안에서 axios 코드를 함수에 넣어서 사용하는 모습인데 이는 async함수가 Promise를 반환하기 때문이다.

axios에 await을 쓰기 위해선 감싸고 있는 함수에 async를 붙여줘야하는데 이 경우 자동으로 Promise를 반환하게 되는데 useEffect Hook에서 반환되는 값은 항상 컴포넌트의 뒷정리 함수여야하기 때문에 문제가 발생한다.
때문에 useEffect에서 async/await을 사용할 때는 반드시 함수를 따로 생성해주어야한다.

아래에 data에 값이 있는지 체크하는 이유는 컴포넌트는 useEffect의 fetchData함수가 동작하는 것보다 렌더링이 먼저 이루어지게 된다.
이 때 datas가 null일 경우 렌더링 시 map함수에서 에러가 발생하기 때문에 미리 유효성 검사를 진행하는 것이다.

usePromise 커스텀 Hook

어플리케이션 개발시 Promise는 정말 많은 곳에서 사용될 수 있는 기능이기 때문에 커스텀 Hook으로 만들어두는 것이 재사용성을 높이는데 도움이 된다.

import { useState, useEffect } from 'react';

export default function usePromise(promiseCreator, deps) {
  // 대기 중/완료/실패에 대한 상태관리
  const [ loading, setLoading ] = useState(false);
  const [ resolved, setResolved ] = useState(null);
  const [ error, setError ] = useState(null);

  useEffect(() => {
    const process = () => {
      setLoading(true);
      try {
        const resolved = await promiseCreator();
        setResolved(resolved);
      } catch(e) {
        console.log(e);
      }
      setLoading(false);
    }
    process();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps);

  return [loading, resolved, error];
}

이 usePromise Hook은 대기 중, 완료결과, 에러의 상태관리를 하며 usePromise의 의존배열 deps를 파라미터로 받는다. 이 deps는 usePromise 내부에서 사용한 useEffect의 의존배열로 설정되게 되는데 이 과정에서 ESLint 에러가 발생하게 되는데 위의 // eslint-disable-next-line react-hooks/exhaustive-deps 이 주석은 해당 에러를 무시하도록 하는 역할을 한다.

출처자료

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=209354690 (리액트를 다루는 기술(개정판))

Categories:

Updated: