typeorm으로 search 구현하기

·2022년 6월 9일
0

팀 프로젝트

목록 보기
30/34

해당 코드가 적혀있는 곳으로 넘어갑니다! 클릭시 이동

572번째 줄


검색엔진이 죽으면 검색이 안되지 않나요?

내가 엘라스틱서치로 검색을 구현해놓고 주차별 검사에서 멘토님에게 들었던 이야기다.

그래서 이부분에 대해서 계속 찾아보고 알아보면서 해결을 해보려고 하다가 도무지 해결이 안되고
시간은 시간대로 흘러서 내려놓고 있었는데, 드디어 오늘!! 해결을 해서 올리는 포스팅이다.

검색하려고 하는 조건

현재 팀플젝의 웹사이트 검색을 하는 방법은 아래처럼 필터를 거치는 방식이다.

제목에 대한 검색이라던가, 내용에 대한 검색 같은 경우에는 like문을 쓰면 바로 해결을 할 수 있지만

이러한 필터 검색의 경우에는 들어오는 값들에 대한 AND 조건으로 검색이 되어야한다.

OR 조건으로 검색하는 것은 정말 손쉽게 로직을 짤 수 있었는데, AND 는 상상이상으로 어려웠다.
그리고 단순하게 게시판 내부에 있는 것이였다면 손쉬웠겠지만,
아래 사진처럼 관계로 맺어진 것을 기준으로 찾아오는 것이었기에 더더욱 어려웠다.

제일 큰 문제가 되었던 것은 바로 검색 필터에 들어오는 값 들이 일정치 않다는 것이였다.
고정적으로 3개만 들어온다면 andWhere을 3번 쓰는 방식으로 해결을 했을텐데
개수가 언제나 달랐기 때문에 저런식으로 해결을 할 수가 없었다.

그러던 와중 이 포스팅을 보게 되었다. => Nest.js TypeORM 리팩터링 (QueryBuilder)

근데 잘 보다보면 forEach로 반복문 을 돌리는 것이 보여서 아! 이거라면 가능하겠다! 라는 생각이 들었고, 바로 코드로 적기 시작했다.

search api

  async searchBoard({ tags, category }) {
    let Ids;
    Ids = getConnection()
      .createQueryBuilder()
      .select('board')
      .from(Board, 'board')
      .leftJoinAndSelect('board.boardSides', 'boardSide')
      .leftJoinAndSelect('boardSide.boardTags', 'boardTag')
      .leftJoinAndSelect('board.place', 'place')
      .leftJoinAndSelect('board.user', 'user')
      .where('1 = 1')
      .orderBy('board.createAt', 'DESC');

    await Ids.andWhere(
      new Brackets((qb) => {
        tags.forEach((tag: string) => {
          qb.orWhere(`boardTag.boardTagName = "${tag}"`);
        });
      }),
    );

    if (category) {
      Ids.andWhere('board.boardSubject = :subject', {
        subject: category,
      });
    }
   const [data, count] = Ids;

    const filter = data.filter((el) => {
      return el.boardSides.length === tags.length ? el : false;
    });

    return [filter, count];
  }

유의할 점🤣
계속적으로 값이 변경되기 때문에 쿼리문의 밑바탕이 되는 Ids에 const를 지정하지 못하였고,
변할 수 있는 let으로 지정했다.

where('1 = 1')이 뭐지?

맨 처음에는 이 코드의 밑바탕이 되는 것을 같이 부캠을 수료했던 수강생분에게 받아서
코드를 해석하던 와중 1=1 이라는 것이 있었다.

그 당시에는 이거 붙이면 좋은데 이유는 아직 잘 모르겠어요. 라고 대답을 하셨고
원하는대로 검색이 안되다보니 이것저것 지우면서 차이를 알게되었는데
선행 조건이 없는 상태로 추가적으로 조건이 붙을 경우에 필요한 요소 로 이해를 하고 있다.

찾아보기를 조건이 정해져있지 않고 변할 수 있는 것을 동적 쿼리 라고 부르는데
동적 쿼리에서 and 조건을 붙일 경우에는 무조건 넣어주는게 좋다고 한다.
반대로 조건이 정해져있는 정적 쿼리에서는 쓰면 안된다.

Brackets이 뭐지?

이것도 createQueryBuilder를 검색하면서 계속 봤던 것이었다.
근데 뭔가 복잡해보여서 사용하질 않았었는데, and 조건 이후에 추가로 조건을 붙일 경우 에는
필연적으로 사용해야하는 문법이라는 것을 확인했고, 그때부터서야 사용하길 시작했다.

사용법은 아래 코드와 같다.

  await Ids.andWhere( // <- andWhere 이후에 추가로 조건문을 걺
      new Brackets((qb) => {
        qb.Where('boardTag.boardTagName = :data',
        { data = "한식" });
        qb.orWhere('boardTag.boardTagName = :data1',
        { data1 = "구로구" });
        qb.orWhere('boardTag.boardTagName = :data2',
        { data2 = "혼술" });
        // qb 속에는 andWhere 조건을 걸었던 Ids의 값이 달려있다.
      }),
    );

결국은 상단의 코드는 아래 코드와 같은 의미를 가지고 있다.
Ids.orWhere('boardTag.boardTagName = :data0',{ data0 = "한식" });
Ids.orWhere('boardTag.boardTagName = :data1',{ data1 = "구로구" });
Ids.orWhere('boardTag.boardTagName = :data2',{ data2 = "혼술" });

그런데 여기서 유심히 봐야할 것이 존재한다.

바로 분명 boardTag.boardTagName 속의 data 임에도 불구하고
넘버링이 달려있는 것을 확인할 수 있는데, 이것은 typeorm의 특성(?)으로

입력값이 구분되어있지 않는다면 맨 마지막에 입력한 값으로 쿼리를 날려버린다.

분명 검색어는 4가지인데, 파라미터는 맨 마지막의 파스타만 존재한다.

이런 결과때문에 검색어의 개수를 알 수 없었던 상황이라 쿼리문 작성을 못하고 있던 상황이였다.

돌려보자 반복문

생각보다 정말 간단했는데, 그냥 브라켓에 forEach를 돌리면 되는 것이였다(....)

  await Ids.andWhere(
      new Brackets((qb) => {
        tags.forEach((tag: string) => {
          qb.orWhere(`boardTag.boardTagName = "${tag}"`);
        });
      }),
    );

브라켓 내부에 forEach를 돌리고 쿼리문들이 모여있는 qb에 orWhere를 tag의 개수만큼 쌓아준다.

여기서 또 주의해야하는 점은 값이 문자열일 경우에 위의 코드처럼 " " 로 감싸줘야한다는 것 이다.

안쓰면 에러나는데 정확한 이유는 SQL 쿼리문에 대한 공부를 더 해야겠다..

여기까지의 쿼리의 결과값 그리고 회고

이 상태에서의 쿼리문은 중간 테이블인 boardSide이 boardTag.boardTagName이 존재했던 개수만큼 값이 들어간 형태로 나온다.


검색을 한 tag는 양식과 한식이였고, 캡쳐를 한 게시글에는 양식,한식,중식의 태그가 들어가있는데
검색을 했던 양식과 한식의 정보만 담겨있는 모습이다.

이렇게만 보면 완성이 된 것처럼 보이지만 한번 더 필터링을 거치는 작업이 필요하다.

왜냐하면 orWhere 조건으로 검색이 되었기 때문에 양식 혹은 한식 이 존재하는 모든 테이블이 검색이 되었기 때문이다.

그래서 원래는 Having(COUNT) GROUP BY 문을 사용해서
tags의 개수와 boardSides의 개수가 일치하는 것을 리턴하려고 했으나

도무지 어떻게 적용을 해야하는지 typeORM 상에서는 모르겠어서
filiter 메소드를 사용하는 것으로 해결을 봤다.

이 부분은 조금 아쉬워서 나중에는 쿼리문 자체로 해결할 수 있는 방안을 생각해볼 예정이다.

   const filter = data.filter((el) => {
      return el.boardSides.length === tags.length ? el : false;
    });

    return filter;

SQL을 다뤄보면서 느끼는 것은 정말 무긍무진하고 해보고 싶은 것이 있다면 무엇이든 기능 자체는 구현이 되어있다는 것이였다.

이건 정말 날을 잡고 공부를 하는 것이 아니라 꾸준하게 공부를 하지 않으면 안된다고 느꼈다.

끝!

profile
물류 서비스 Backend Software Developer

0개의 댓글