dolog

비동기 처리 : callback에서 async, await으로 본문

톺아보기

비동기 처리 : callback에서 async, await으로

dokite 2024. 5. 7. 00:12

목차

  1. mysql과 async ・ await
  2. mysql vs mysql2
  3. row를 배열 구조 분해 할당하는데 안되었던 이유와 해결 방법(feat. interface)
  4. createConnection()에서 createPool()로 바꾼 이유
  5. 비동기와 동기
  6. 비동기의 삼총사 : callback, Promise, async ・ await

 

mysql과 async ・ await

 

기존 코드를 먼저 보여드리자면, 회원가입 로직에서 이미 존재하는 유저가 있는지 확인하는 query에 존재하는 유저가 없을 때 계정을 추가해 주는 query를 콜백함수로 넘겨주었습니다. 

import { Request, Response } from 'express';
import { connection } from '../..';

export const createAccount = (request: Request, response: Response) => {
	const { email, phoneNumber, password } = request.body;
	const checkExistingUserQuery = 'SELECT email FROM User WHERE email=?';
	const registerUserQuery = 'INSERT INTO User SET ?';

	connection.query(checkExistingUserQuery, [email], (error, rows) => {
		if (error) return response.status(500).send('Internal Server Error');

		if (rows.length) {
			return response.status(400).send('Bad Request');
		}

		connection.query(
			registerUserQuery,
			{ email, phoneNumber, password },
			(error, rows) => {
				if (error) return response.status(500).send('Internal Server Error');

				return response.status(201).send('User registered successfully');
			}
		);
	});
};

 

중첩 깊이가 깊지 않아서 괜찮지만 계정 생성 쿼리에도 콜백함수가 추가되는 등 깊이가 깊어지게 된다면 코드를 한눈에 파악하기 힘든 건 물론 아마 에러 핸들링도 쉽지 않을 겁니다. 따라서 async ・ await으로 깔끔하게 변경해 줍니다.

 

import { Request, Response } from 'express';
import { FieldPacket, RowDataPacket } from 'mysql2';
import { db } from '../../db';

interface CreateAccount extends RowDataPacket {
	id: number;
	email: string;
	phoneNumber: string;
	password: string;
}

export const createAccount = (request: Request, response: Response) => {
	const promisePool = db.promise();
	const { email, phoneNumber, password } = request.body;
	const checkExistingUserQuery = 'SELECT email FROM User WHERE email=?';
	const registerUserQuery = 'INSERT INTO User SET ?';

	db.getConnection(async (error, connection) => {
		try {
			const [rows]: [CreateAccount[], FieldPacket[]] = await promisePool.query(
				checkExistingUserQuery,
				[email]
			);
			if (rows.length) return response.status(400).send('Bad Request');

			await promisePool.query(registerUserQuery, {
				email,
				phoneNumber,
				password,
			});

			return response.status(201).send('User registered successfully');
		} catch (error) {
			return response.status(500).send('Internal Server Error');
		} finally {
        		// 연결을 했으면 반드시 해제해 주어야 합니다.
			return db.releaseConnection(connection);
		}
	});
};

 

query가 분리되니 훨씬 파악하기 쉽고, try/catch 문으로 에러 처리도 catch 문에서 직관적으로 처리할 수 있습니다.

 

mysql vs mysql2

 

mysql에서는 Promise를 지원하지 않아서 promise-mysql 모듈을 설치해야하는데 mysql2에서는 Promise를 지원하여 따로 모듈을 설치하지 않아도 된다고 하길래 mysql2를 설치해서 사용했습니다.

또한 npm trends를 살펴보면 mysql2는 지속적으로 업데이트되고 있고, 사용량도 훨씬 많아지고 있습니다.

(참고: https://npmtrends.com/mysql-vs-mysql2)

 

createConnection()에서 createPool()로 바꾼 이유

 

createConnection의 단점은 단일 연결을 하기 때문에 계속 연결된 상태를 유지한다면 서버 과부하가 일어나거나 리소스 낭비가 됩니다.

따라서 createConnection은 연결할 때마다 또 다시 생성하고 연결을 종료해줘야 하는 번거로움이 있습니다.

 

그에 반해 createPool은 여러 개의 연결을 만들어서 pool에 저장합니다.

필요할 때마다 pool에서 가져와 사용하고 다시 반환하는 방식으로 createConnection의 단일 연결 방식의 문제점을 해결해 줍니다.

 

row를 배열 구조 분해 할당하는데 안되었던 이유와 해결 방법(feat. interface)

 

이미 존재하는 유저가 있는지 확인하기 위해서 쿼리 결괏값(rows)을 배열에 담아 처리하려고 했는데 에러가 발생했습니다.

타입 체크에서 발생한 에러

 

찾아본 결과 interface 확장으로 에러를 해결할 수 있었습니다.

(참고 : https://stackoverflow.com/questions/54583950/using-typescript-how-do-i-strongly-type-mysql-query-results)

인터페이스 확장으로 에러 해결

 

RowDataPacket가 배열을 반환하는데도 불구하고 꼭 interface를 확장해야만 하는지 궁금했는데 RowDataPacket의 타입이 느슨해서 interface를 확장해줘야 한다...라는 정보만 얻을 수 있었습니다. 추가로 알아본 결과 RowDataPacket 자체가 낮은 수준에서 DB 시스템과 상호 작용하도록 설계되어서 기본적인 인터페이스만 제공하기 때문에 결국 RowDataPacket를 확장해 주어야 원하는 결과를 얻을 수 있던 것입니다.

(참고: https://github.com/mysqljs/mysql/issues/1330)

 

 

createConnection()에서 createPool()로 바꾼 이유

 

createConnection의 단점은 단일 연결을 하기 때문에 계속 연결된 상태를 유지한다면 서버 과부하가 일어나거나 리소스 낭비가 됩니다.

따라서 createConnection은 연결할 때마다 또다시 생성하고 연결을 종료해줘야 하는 번거로움이 있습니다.

 

그에 반해 createPool은 여러 개의 연결을 만들어서 pool에 저장합니다.

필요할 때마다 pool에서 가져와 사용하고 다시 반환하는 방식으로 createConnection의 단일 연결 방식의 문제점을 해결해 줍니다.

 

▷ 사실 Socket이 아닌 HTTP 통신은 일회성 요청이기 때문에 createConnection을 사용해도 큰 이슈가 생기지는 않습니다.

추후 Socket을 사용하게 된다면 createPool을 사용해서 얻은 장점과 단점 등 사용한 후기(?)를 작성해 보도록 하겠습니다.

 

비동기와 동기

 

비동기는 무엇인가?

어떤 작업의 실행 결과를 기다리는지, 기다리지 않는지에 따라 동기와 비동기로 나눠집니다.

동기는 기다리고, 비동기는 기다리지 않습니다.

 

그렇다면 왜 비동기에 대해서 알아야 할까?

자바스크립트는 싱글 스레드 환경입니다. 이 말은 한 번에 한 가지 일밖에 못 한다는 말인데, 만약 어떤 작업이 1억 년이 걸리는 일이라면 1억 년 후에 다른 작업을 할 수 있습니다.

그러다 보니 시간이 얼마나 걸리지 몰라 실행 결과를 기다리지 않아도 되는 비동기가 필요했습니다.

 

비동기 삼총사 : callback, Promise, async ・ await

 

callback은 무엇인가?

자바스크립트에서 함수의 인수로 전달하는 함수를 콜백함수(callback function)라 합니다.

주로 Promise가 도입되기 전에 callback을 사용하여 비동기 작업을 처리했습니다.

하지만 callback이 중첩될수록 코드의 가독성이 나빠지고 에러 처리가 힘들어졌습니다.(일명 콜백 지옥)

 

Promise는 무엇인가?

Promise는 콜백함수의 문제점을 해결하기 위해 나온 방법입니다.

Promise는 어떤 작업에 대한 결과를 가지고 있는데 이 결과를 가지고 작업하기 위해 then과 catch를 사용합니다.

일반적으로 then에서 결과 처리를 하고, catch에서 에러 처리를 합니다.

또한 Promise에서 제공하는 then, catch를 사용해서 연속적인 처리가 가능한데 이 또한 깊이가 생기면 콜백 지옥과 비슷한 문제점들이 생깁니다.

 

callback과 Promise의 차이점

callback은 결괏값을 반환합니다.

Promise는 다음에 수행할 수 있는 작업을 반환합니다.

 

 추가로 보면 좋을 영상

https://youtube.com/shorts/_m5-EG-t10Q?si=NT2w_cWnE6HWbKov

 

 

async ・ await은 무엇인가?

Promise와 똑같이 Promise를 반환하지만 async ・ await 키워드를 사용해서 좀 더 깔끔하게 비동기 코드를 작성할 수 있습니다.