JavaScript 비동기 코드를 "잘" 쓰는 방법

강진석·2022년 4월 11일
3
post-thumbnail

블로그 첫 포스팅을 뭘 올릴까 고민 중에 커리어리에서 자바스크립트에 관한 글을 읽게 되었다.
본문이 영어로 작성되어 있기도 하고, 알아두면 자주 써먹을 지식인 것 같아 해석과 함께 잘 정리해 두려고 한다.
출처: 14 Linting Rules To Help You Write Asynchronous Code in JavaScript

JavaScript에서 비동기 코드를 작성하는 데 도움이 되는 14가지 규칙

모든 항목은 EsLint에서 제공되는 기능이며, 이를 사용하지 않더라도 비동기 코드를 더 잘 이해하는데 도움이 될 것이다.

1. async 함수를 new Promise 생성자 안에서 사용하지 않기

// ❌
new Promise(async (resolve, reject) => {});

// ✅
new Promise((resolve, reject) => {});

Promise 생성자 안에서 async 함수를 쓰는 것은 문법적으로는 문제가 없지만 다음과 같은 두가지 문제를 야기한다.

  1. async 함수가 던진 에러는 Promise에 reject 되지 않는다.
  2. 후에 다시 코드를 보았을 때 new Promise 생성자가 불필요해 보이며 제거할 수도 있다.

2. 반복문 안에서 await 사용하지 않기

// ❌
for (const url of urls) {
  const response = await fetch(url);
}

// ✅
const responses = [];
for (const url of urls) {
  const response = fetch(url);
  responses.push(response);
}

await Promise.all(responses);

매 반복이 순차적으로 수행되어야 할 수도 있지만, 그렇지 않은 경우에는 매우 비효율적으로 매 작업을 기다려야한다. 비동기함수를 통해 병렬적으로 수행시켜 성능을 향상시키자.

3. Promise 생성자 안에서 return 사용하지 않기

// ❌
new Promise((resolve, reject) => {
  return result;
});

// ✅
new Promise((resolve, reject) => {
  resolve(result); 
  //or 
  reject(error);
});

Promise 생성자 안에서는 return을 통해 값을 반환할 필요가 없다. 값을 전달해야 할 때는 resolve, 에러가 발생했다면 reject를 사용하자.

4. 변수의 변경은 한번에 하나씩 진행하기

// ❌
let totalPosts = 0;

async function getPosts(userId) {
 const users = [{ id: 1, posts: 5 }, { id: 2, posts: 3 }];
 await sleep(Math.random() * 1000);
 return users.find((user) => user.id === userId).posts;
}

async function addPosts(userId) {
 totalPosts += await getPosts(userId);
}

await Promise.all([addPosts(1), addPosts(2)]);
console.log('Post count:', totalPosts);

처음 5가 더해지고, 3이 더해져 8이 될 것 같지만, 결과는 3이나 5가 출력될 것이다. 변수에 값이 쓰여지는 시점보다 두번째 함수가 변수를 읽는 시점이 더 빠르기 때문이다.

// ✅
async function addPosts(userId) {
 const posts = await getPosts(userId);
 totalPosts += posts; // variable is read and immediately updated
}

변수를 읽어 오는 동시에 업데이트해 문제를 피할 수 있다. 경쟁 조건이 발생할 경우 잘 처리하자.

5. 중첩 콜백에 주의하기

// ❌
async1((err, result1) => {
 async2(result1, (err, result2) => {
   async3(result2, (err, result3) => {
     async4(result3, (err, result4) => {
       console.log(result4);
     });
   });
 });
});

// ✅
const result1 = await asyncPromise1();
const result2 = await asyncPromise2(result1);
const result3 = await asyncPromise3(result2);
const result4 = await asyncPromise4(result3);
console.log(result4);

끝도 없이 이어지는 콜백의 중첩을 콜백 지옥이라 하고, 이는 코드의 가독성이나 유지관리를 힘들게 한다. 애초에 콜백보다 async/await을 활용하자.

6. async 함수에서 await을 리턴하지 않기

// ❌
async () => {
  return await getUser(userId);
}

// ✅
async () => {
  return getUser(userId);
}

어짜피 async 함수의 리턴은 promise에 래핑되므로, promise를 반환하는 비동기 함수를 굳이 await 하지 않아도 promise를 직접 반환할 수 있다. 단 try/catch 문에서는 promise의 reject를 catch할 수 없으므로 예외이다.

7. Promise에서 reject 시 Error 객체를 사용하기

// ❌
Promise.reject('An error occurred');

// ✅
Promise.reject(new Error('An error occurred'));

에러 객체를 사용하면 에러 추적 스택에 저장되기 때문에 오류가 발생한 위치를 더 쉽게 알 수 있다.

8. Node.js - 콜백 내에서 에러를 처리하기

// ❌
function callback(err, data) {
  console.log(data);
}

// ✅
function callback(err, data) {
  if (err) {
    console.log(err);
    return;
  }

  console.log(data);
}

콜백에 전달되는 에러는 미루거나 떠넘기지 말고 콜백 함수의 맨 처음에서 처리하고 넘어가야 한다.

9. Node.js - 콜백의 첫번째 prop은 에러

// ❌
cb('An error!');
callback(result);

// ✅
cb(new Error('An error!'));
callback(null, result);

콜백의 첫번째 인수는 에러라는 규칙을 명심하자. 에러를 String으로 전달하거나, 에러를 전달할 필요가 없는 경우에 그 자리에 다른 prop를 전달하지 말고 null이나 undefinded를 사용하여 자리를 채워주어야 한다.

10. Node.js - 비동기 Method 위주로 사용하기

// ❌
const file = fs.readFileSync(path);

// ✅
const file = await fs.readFile(path);

fs.readFileSync는 fs.readFile와 같은 작업을 수행하는 동기 메소드이다. 문제는 Node.js에서 I/O 작업에 동기 메서드를 사용 하면 이벤트 루프가 차단된다. 꼭 동기 메소드를 사용해야 하는 경우가 아니라면 비동기 메소드를 사용하자.

TypeScript에서는 Promise의 타입을 확인할 수 있기 때문에 JavaScript에서 허용되는 여러 실수에 대해 더 상세한 eslint 규칙 설정이 가능하다!

11. TypeScript - 비동기가 아닌 함수에 await 쓰지 못하게 설정

// @typescript-eslint/await-thenable

// ❌
function getValue() {
  return someValue;
}

await getValue();

// ✅
async function getValue() {
  return someValue;
}

await getValue();

이 규칙은 JavaScript에서는 단순히 코딩 실수로 분류되는 Promise가 아닌 함수를 await 하는 것을 허용하지 않는다.

12. TypeScript - Promise에서 항상 에러를 catch하도록 설정

// @typescript-eslint/no-floating-promises

// ❌
myPromise()
  .then(() => {});

// ✅
myPromise()
  .then(() => {})
  .catch(() => {});

이 규칙은 Promise 이후에 반드시 .catch() 구문을 추가하게 함으로써 잠재적인 오류를 항상 처리하도록 한다.

13. TypeScript - Promise를 처리할 수 없는 구문에 작성하지 못하도록 설정

// @typescript-eslint/no-misused-promises

// ❌
if (getUserFromDB()) {}

// ✅ 👎
if (await getUserFromDB()) {}

// ✅ 👍
const user = await getUserFromDB();
if (user) {}

이 규칙은 조건문의 조건절과 같은 Promise를 처리할 수 없는 구문에 Promise가 위치하지 못하도록 설정한다. await 을 추가하여 Promise가 반환한 값을 기다리도록 하는 것은 가능하지만, 코드의 가독성을 생각한다면 그 이전에 변수에 반환된 결과값을 저장한 후 이를 사용하는 것이 좋을 것이다.

14. TypeScript - Promise를 반환하는 함수에 async를 붙이도록 설정

// @typescript-eslint/promise-function-async

// ❌
function doSomething() {
  return somePromise;
}

// ✅
async function doSomething() {
  return somePromise;
}

이 규칙은 Promise를 반환하는 함수를 비동기 함수로 선언하도록 설정한다. JavaScript에서 비동기 함수에 async를 깜빡하고 적지 않아 Promise 객체 자체가 반환되어 전달받은 함수에서 에러가 발생하게끔 코드를 작성하는 경우도 잦은데, 문법 상의 오류가 아니어서 찾기 힘들 수도 때문에 유용하다고 생각한다.

0개의 댓글