knex와 thenable

White Piano·2023년 8월 22일
0

db-migration에도 흥미가 있고, optional sql때문에라도 기존의 raw sql에서 탈피해 query builder를 사용해 보고자 문법을 공부하는 과정에서 큰 혼란을 겪었다.

knex

const query = knex
	.where('id', id)
	.select('*')
	.from('user')
	.limit(pageSize);

단순한 예제에서 익숙한 functional chaining을 볼 수 있었다. chain을 구성하는 함수 이름이 굉장히 직관적이어서 만족스러운 코드를 만들 수 있다.

query.then( user => doSomethingWithUser(user));

읽은 data는 위와 같은 형식으로 접근이 가능하다. 그런데 뭔가 이상하다. 알 수 없는 위화감에 불안했지만, 문서를 계속 읽어나갔다.

optional query

처음 언급한 대로 kenx를 사용하면서 optional query가 좀 더 수월해질 거라 기대했다. 현재 내가 작성한 raw sql은 사용자의 입력에 따라 filtering과 sorting을 하기 상당히 귀찮았다.

raw sql을 사용한 기존 코드

전체 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가 동작할 수 없다.

그렇다면 knexwhere까지는 sql을 build 하기만 하고 select와 동시에 실행하는 걸까? 만약 그런 거라면 위 코드처럼 조건부로 limit을 추가할 순 없다. 게다가 DB에서 모든 값을 읽어온 다음에 sorting과 filtering을 진행한단게 아닌가? 너무나도 비효율적이다. 겨우 그 정도밖에 안 되는 프로그램일거라고 생각하지 않는다. 분명 무언가 놓치고 있다. 하지만 문법적으로 promise는 할당과 동시에 background에서 실행될 터였다. 결론적으로 위 함수의 연쇄는 promise가 아니다.

thenable

지식의 보고 stackoverflow에 내가 하고자 한 질문이 이미 있었다. 실행 시점실행 순서에 관한 답변 모두 thenable에 관해 얘기하고 있다.

이전 Elm을 공부하면서 경험했던 함수형 언어 특유의 chaining이 많이 그리웠다. thenable을 활용하면 js에서도 비슷한 방식으로 코드를 작성할 수 있을 것 같다. pipe operator처럼 다양한 방법을 허용하는 js의 자유로움에 다시 한번 감사한다. 다만, 기쁘기만 했던 예전과 달리 이 글을 읽은 뒤로 "js를 사용한다는 것"에 대해 고민하게 된다.

0개의 댓글