db-migration에도 흥미가 있고, optional sql때문에라도 기존의 raw sql에서 탈피해 query builder를 사용해 보고자 문법을 공부하는 과정에서 큰 혼란을 겪었다.
const query = knex
.where('id', id)
.select('*')
.from('user')
.limit(pageSize);
단순한 예제에서 익숙한 functional chaining을 볼 수 있었다. chain을 구성하는 함수 이름이 굉장히 직관적이어서 만족스러운 코드를 만들 수 있다.
query.then( user => doSomethingWithUser(user));
읽은 data는 위와 같은 형식으로 접근이 가능하다. 그런데 뭔가 이상하다. 알 수 없는 위화감에 불안했지만, 문서를 계속 읽어나갔다.
처음 언급한 대로 kenx를 사용하면서 optional query가 좀 더 수월해질 거라 기대했다. 현재 내가 작성한 raw sql은 사용자의 입력에 따라 filtering과 sorting을 하기 상당히 귀찮았다.
전체 task를 읽는 대신 원하는 progress에 있는 task만 읽어오도록 하기 위해 sql IN
을 사용했다. 다만 원하는 유형이 1가지 일수도, 2가지일 수도 있기 때문에 "?".repeat(progress.length).split("").join(", ");
이라는 불편한 코드가 포함되어야 했다.
정렬 기준 또한 사용자의 입력에 따르는데, 전달하는 값이 value가 아닌 column 이라서 conn.escapeId
를 사용해야 했다. mysql
package에서는 ??
를 사용할 수 있는데, mysql2
package에서는 지원하지 않는 듯하다.
findByUser(
conn: PoolConnection,
user_id: TaskDTO["user_id"],
{ sort, filter: { progress } }: SearchOption
) {
const sql = `
SELECT
id,
progress,
user_id,
content,
memo,
deadline,
registerd_at,
started_at,
finished_at
FROM
task
WHERE
user_id = ?
${
progress
? `AND progress IN (${"?"
.repeat(progress.length)
.split("")
.join(", ")})`
: ``
}
ORDER BY
${sort ? conn.escapeId(sort) : "NULL"};
`;
return new Promise<TaskDTO[]>((resolve, _reject) => {
conn
.execute<RowDataPacket[]>(sql, [user_id, ...(progress ?? [])])
.then(([taskEntityArr]) => {
const taskDTOs: TaskDTO[] = taskEntityArr.map((taskEntity) => {
const { id, user_id, progress, content, memo, deadline }: TaskDTO =
taskEntity as TaskEntity;
return { id, user_id, progress, content, memo, deadline };
});
return resolve(taskDTOs);
});
});
}
그럼, 위 코드에 knex를 도입하면 어떤 코드가 나올까? option
에 따라 limit
을 추가해주면 되겠지?
const query = knex
.where(/*...*/)
.select(/*...*/);
if(hasSortOption) query.limit(option.sort) // ...?
const result = await query(); // 어라?
생각해 보면 knex는 함수의 연쇄로 구성된다. 그렇단 말은
`knex`가 resolve되고 그 결과를 `where`가 넘겨받는다.
`where`가 resolve되고 `select`가 이어진다.
`select`가 완료되면 data를 반환한다.
...?
아무리 생각해도 그럴 순 없다. 우선 sql이 완성되지 않은 시점에 query가 동작할 수 없다.
그렇다면 knex
와 where
까지는 sql을 build 하기만 하고 select
와 동시에 실행하는 걸까? 만약 그런 거라면 위 코드처럼 조건부로 limit
을 추가할 순 없다. 게다가 DB에서 모든 값을 읽어온 다음에 sorting과 filtering을 진행한단게 아닌가? 너무나도 비효율적이다. 겨우 그 정도밖에 안 되는 프로그램일거라고 생각하지 않는다. 분명 무언가 놓치고 있다. 하지만 문법적으로 promise는 할당과 동시에 background에서 실행될 터였다. 결론적으로 위 함수의 연쇄는 promise가 아니다.
지식의 보고 stackoverflow에 내가 하고자 한 질문이 이미 있었다. 실행 시점과 실행 순서에 관한 답변 모두 thenable에 관해 얘기하고 있다.
이전 Elm을 공부하면서 경험했던 함수형 언어 특유의 chaining이 많이 그리웠다. thenable
을 활용하면 js에서도 비슷한 방식으로 코드를 작성할 수 있을 것 같다. pipe operator처럼 다양한 방법을 허용하는 js의 자유로움에 다시 한번 감사한다. 다만, 기쁘기만 했던 예전과 달리 이 글을 읽은 뒤로 "js를 사용한다는 것"에 대해 고민하게 된다.