logo

Post

async/awiat는 언제나 옳은가?

게시글 대표 이미지

상황

주어진 상황은 다음과 같다.

핸들러 함수(handleIdChange)에 의해 id가 바뀔 때마다 내부 동작이 실행된다. 내부 동작이란 getData를 비동기적으로 데이터를 요청하고 불러오는 것이다.

Code (시작)

// ...
async function getData(id) {
  // 생략된 복잡한 fetching 로직...
  const res = await fetch(`${baseUrl}/${id}`)
  const json = await res.json()

  return json
}

async function handleIdChange(e) => {
  const id = e.target.value
  const product = await getData(id)
  // ...
}

위 상황을 보면 어떠한 리팩토링 욕구가 생긴다. 바로 getData 분리. 생략된 복잡한 fetching 로직이라는 것이 상당히 별도로 빼내고 싶으니까.

Code (리팩토링1 - 함수 분리)

// ...
async function fetchData(id) {
  // 생략된 복잡한 fetching 로직...
  const res = await fetch(`${baseUrl}/${id}`)
  const json = await res.json()
  
  return json
}

async function getData(id) {
  const json = await fetchData(id)

  return json
}

async function handleIdChange(e) => {
  const id = e.target.value
  const product = await getData(id)
  // ...
}

이제 새로운 async 함수가 또 생겨버렸다. 그런데 잘 보면 getData는 더 이상 async 키워드가 필요 없게 되었다.

async/await 키워드를 사용한다는 건 어떤 값을 전달받든 Promise를 반환하도록 한다는 뜻이다. 원래부터 Promise라면 그대로 전달하지만 Promise가 아닌 경우에도 Promise로 만들어서 전달하겠다는 의미라고 해석할 수 있다.

getDatafetchData가 호출한 응답값을 json 형태로 그대로 반환하는 역할만을 하고 있다. 그런데 fetchDataasync 함수이기 때문에 응답은 Promise 객체이다. 즉 Promise를 받아서 Promise를 전달해줄 뿐인데, 다시 말해 이미 Promise를 전달받고 있으니 async 키워드가 필요없다는 것이다.

Code (리팩토링2 - async 생략)

// ...
async function fetchData(id) {
  // 생략된 복잡한 fetching 로직...
  const res = await fetch(`${baseUrl}/${id}`)
  const json = await res.json()
  
  return json
}

function getData(id) {
  const json = fetchData(id)

  return json
}

async function handleIdChange(e) => {
  const id = e.target.value
  const product = await getData(id)
  // ...
}

한편 fetchData도 같은 맥락에서 async 키워드를 삭제해도 된다. 왜냐하면 fetch 함수도 Promise를 반환하기 때문이다.

덧붙여 fetchPromise를 반환한다면 이 정도 간단한 변환엔 then을 이용하는 것도 가독성 측면에서 좋아보인다.

그런데 그러고보니 fetchData는 응답값을 받은 것을 그대로 반환할 뿐인 함수다. async 함수이기 때문에 Promise가 아니라면 Promise로 변환하겠지만 이미 응답값이 Promise니까 위와 마찬가지의 논리로 async 키워드를 삭제할 수 있다.

Code (리팩토링3 - then)

// ...
function fetchData(id) {
  // 생략된 복잡한 fetching 로직...
  return fetch(`${baseUrl}/${id}`).then(res => res.json())
}

function getData(id) {
  const json = fetchData(id)

  return json
}

async function handleIdChange(e) => {
  const id = e.target.value
  const product = await getData(id)
  // ...
}

한편 선언문으로 작성된 위 함수들을 화살표 함수로 바꾸게 되면 좀 더 깔끔하게 코드를 작성할 수 있다.

Code (리팩토링4 - arrow function)

// ...
const fetchData = (id) => fetch(`${baseUrl}/${id}`).then(res => res.json())

const getData = (id) => fetchData(id)

const handleIdChange = async (e) => {
  const id = e.target.value
  const product = await getData(id)
  // ...
}

정리

  • 중간 과정에서는 async/await가 없어도 괜찮다.
    • 마지막에 전달되는 최종 결과물이 Promise이기만 하면 그 종착지에서 비동기 처리가 이뤄질 것이다. Promise로 넘어온다는 것만 확인되면 그 중간 과정에서는 async 키워드는 불필요하다.
  • 간단하게 then으로 비동기 처리할 수 있는 경우에도 async를 생략할 수 있다.

결론

async/await는 기존 비동기 처리가 주는 불편함을 해소하기 위해서 등장한 만큼 가독성 좋은 코드를 작성하는데 매우 유용하다. 그렇지만 중간 과정의 모든 함수에서 사용해야 하는 것은 아니다.

한편 그럼에도 불구하고 "그 모든 중간 과정에 async를 남발하더라도 동작에 문제가 생기진 않는다"고 반문할 수도 있겠다. 사실 문제는 없다. 다만 async 키워드로 함수를 만든다는 것의 의미를 재고해볼 여지는 있다.

async 함수는 원래의 함수를 Promise로 감싸는 것

사실 Promise로 감싼다고 하여 심각한 성능 저하가 발생한다거나 하지는 않는다. 하지만 비동기 제어가 반드시 필요하지 않음에도 그렇게 보인다는 문제가 있다. 코드는 항상 나만 보는 것이 아님을 고려할 때 가급적이면 최대한 오해의 소지가 적은 코드를 쓰는 게 좋지 않을까? 그렇지 않으면 협업자(미래의 나를 포함한)들은 해당 함수를 `비동기 처리를 동기적으로 마친 후에야 다음 코드로 진행해야 한다'는 오해를 할 수 있으니까.

물론 함수 내부에서 await가 여러 개가 있는 경우엔 이런 방식이 통하지 않는다.

지금까지 다룬 내용이 성능과 가독성 측면에서 대단히 중요해서 반드시 다뤄져야 할 이슈는 아니다. 그럼에도 더 나은 코드를 지향하자는 관점에서 머릿속에 새겨두면 꽤나 괜찮을 것 같다. 그렇게 어려운 내용도 아니니까!

출처

이 글은 아래 영상을 시청한 후 정리하여 재구성한 글입니다.

[Javascript 미세팁] 비동기처리는 무조건 async/await 아닌가요? - FE재남

다른 글 읽기
이전 글
  • Cover Image for null 병합 연산자 (??)
      null 병합 연산자 (??)

      "||" 이면 충분한 거 아니야?!

    • 다음 글
    • Cover Image for await vs return vs return await
        await vs return vs return await

        async 함수를 작성할 때 사용하는 await, return, return await는 각기 다른 결과를 낳게 된다는데... 알고 쓰고 있니?