티스토리 뷰

예전부터 관심이 있었던 “대학시간” 프로젝트에서 운영과 개발자를 이어서 할 생각이 있는 사람을 뽑는다는 공지에서부터 시작 되었다.

학기 초마다 애용하던 서비스였고, ReactJS 를 사용한 프로젝트라니 그냥 지나갈 수가 없었다.

크롤러 기여

가장 먼저 주어진 과제는 수강신청 데이터를 불러오는 크롤러를 개선시키는 것이다. 보통 크롤링을 할 때 puppetter 을 많이 사용한다. 가상 브라우저를 만들어 우리가 브라우저를 사용하는 것처럼 사용할 수 있어 직관적이고 사용하기가 쉽다. 하지만, 치명적인 단점이 있는데, 속도가 많이 느리고 페이지마다 로딩을 기다리는 것이 장점이, 그리고 단점이 될 수 있다.

 

보통 puppetter 은 직 링크를 통해 요청을 보낼 수 없거나, Client Side Rendering (CSR) 방식인 사이트를 크롤링을 해야할 때 보통 사용을 한다.

 

우리 학교 인트라 넷인 히즈넷인 경우 PHP 개발 스택으로 이루어 져있으며, 보안적으로 체계적이지 않아 단순 요청 라이브러리인 axios 같은 것으로 충분히 빠르게 할 수 있다.

 

이번 과제는 단 2개의 요청으로 가져올 수 있는 데이터였다.

데이터 후처리를 하면서 많은 것을 배울 수 있었는데, 나는 아직까지 no-SQL 방식에 맛이 들려서 노드와 SQL은 한번도 사용해본적이 없다. 그래서인지 sequelize 라이브러리를 들어보기만 했지 사용해본적이 없었는데, SQL 을 사용하는 이 프로젝트에서 sequelize 를 사용하고 있어 도입된 방법을 보면서 익혀나갈 수 있었다.

백엔드 카운터 만들기

이제부터 본격적으로 기능 추가를 위해서 기존에 필기 해 두었던 것을 읽어보았다. 매주 화요일 10시마다 회의가 있는데, 마지막으로 나온 내용이 팀원들이 프론트 엔드 (ReactJS) 는 배워보고 싶으나 아직 백엔드에 익숙하지 않아 기능 추가가 힘들 것 같다는 내용이었다.

 

그래서 프론트는 일단 해보고 싶은 사람이 많으니 잠시 미뤄 두고 백엔드에 집중하기로 결정했다.

 

추가/즐겨찾기/이삭줍기 기능

 

추가/즐겨찾기/이삭줍기 카운트 추가 · Issue #48 · zoomkoding/college-timetable

사용자의 편의를 위해서 해당 과목을 몇명이 시간표에 추가했고, 북마크는 몇명이 하였고, 이삭줍기는 몇명이 하고 있는지 정보를 알려주는 시스템이 있으면 좋을 것 같습니다. 만약에 마음에

github.com

첫번째로 작업할 기능은 추가/즐겨찾기/이삭줍기 기능을 위한 백엔드 구축이다.

이런식으로 검색 결과에 몇명이 추가/북마크/이삭줍기 했는지 확인하는 기능을 추가하기 위해 DB 에서 관련 데이터를 가져와 가공하여 클라이언트에 보내주는 작업을 해야했다.

 

처음부터 자바스크립트를 배울 때 MongoDB 와 Firebase 등 NoSQL 데이터베이스와만 작업을 했다. 그 결과, 한번도 SQL 과 작업을 해본적이 없었고 친구들이 백엔드 프로젝트로 SQL 를 사용하여 진행했을 때 보고 해봐야지 하면서도 SQL 의 매력을 느끼지 못해 계속 미뤄두었다.

 

이번 프로젝트를 하면서 sequelize 를 사용해보고 반강제로(?) 배워보기를 기대하고 있고, 그리고 프로젝트를 담당하시는 선배님께서 sequelize 에 경험이 많은 것 같아 노하우를 전수 받기를 기대하면서 시작을 했다.

작업

작업 해야하는 내용은 간단하다! 일단 먼저 유저가 사이트에서 검색을 했을 때 어떤 요청을 보내는지 확인하고 해당 endpoint 를 검색해 담당하는 코드를 찾으면 바로 시작할 수 있다.

 

// 기존 코드
exports.getSearchResults = async (req, res) => {
  const { search, page } = req.query;
  const limit = +process.env.PAGE_LIMIT;
  const decodedSearch = decodeURIComponent(search);

  Search.create({ userId: req.user.id, search: decodedSearch });
  const { count, rows: lectures } = await Lecture.findAndCountAll({
    where: searchWhereClause(decodedSearch),
    limit,
    offset: page ? limit * (+page - 1) : 0,
  });

  res.send({ pages: count === 0 ? 0 : Math.ceil(count / limit), lectures });
};

위 코드가 데이터베이스에서 수업(Lecture) 정보를 찾아 유저에게 반환시켜주는 코드이다. sequelize을 한번도 사용 안해봐서 처음보는 함수가 여럿 있었다. 개인적으로 아직 자바스크립트로 객체를 만들어 사용을 해본적이 없는 나에게 이번에 배울 것을 너무 많았다.

 

일단 이 코드에 작성된 모든 코드를 읽어보면서 대략적으로 어떻게 작동이 되는지 감을 잡을 수 있었다.

 

프로젝트를 확인해보니, 잘 만들어진 모델이 있고, 모델이 있으면 그 객체를 가져와 함수를 사용할 수 있다는 것을 확인할 수 있었다. 이 전에 프로젝트를 보며 개발 환경을 만드는 작업을 했을 때 자동으로 관련 테이블을 생성해주는 sequelize 에 감탄한 적이 있다. 모델에 정의를 하면 자동으로 만들어주는 것이 너무 신기해 앞으로 개인 프로젝트를 하게 된다면 꼭 한번 써봐야하겠다는 생각이 들었다.

 

가장 먼저 이삭줍기(spike) 카운트를 넣는 것을 도전했다. 일단 어떤 함수가 있는지 몰라 더큐먼트를 간단하게 읽어보고, 가장 그럴듯한 count() 함수를 찾을 수 있었다. DB 에서 관련 테이블을 찾고 비슷한 이름을 가진 모델을 찾아 무지성으로 위에있는 함수와 최대한 비슷하게 함수를 작성해보았다.

const {count: spikeCnt} = await UserLectureGleaningRelation.count({
  where: { lectureId: lec.id },
});
console.log(spikeCnt); // undefined

사실 관련 문서를 잘 읽었다면 반환되는 변수는 오브젝트가 아니라는 것을 쉽게 알 수 있고, 그냥 변수라는 것을 알 수 있었을 것이다. 하지만, 그것을 빨리 확인하지 못했던 나는 코드 결과를 보고 나서야 알 수 있었다.

const spikeCnt = await UserLectureGleaningRelation.count({
  where: { lectureId: lec.id },
});
console.log(spikeCnt); // ✅

잘 작동 된다. 진짜 함수 이름을 직관적으로 잘 지어야 한다는 것을 느낄 수 있었다.

 

북마크도 비슷한 방식으로 만들 수 있었다. 하지만, 문제가 된 것은 시간표에 추가한 인원 이었다.

 

해당 서비스에는 한 사람이 여러개의 시간표를 만들 수 있다. 따라서, 이전 이삭줍기와 북마크는 한사람당 하나의 수업만 저장할 수 있어 그냥 개수를 세면 되었지만, 추가된 인원은 한 사람당 한번만 카운트가 되어야 하고, 수업 ID, 시간표 ID, 유저 ID 가 모두 다른 위치에 저장되어 있어 JOIN 과정도 필요했다.

 

sequelize 으로 JOIN 역시 해본적이 없어 프로젝트에서 관련 코드를 검색하여 읽어 봤다. 일단 JOIN 이라고 하지 않고, include 하는 것을 확인할 수 있었으며, 무조건 할 수 있는 것은 아니고, 해당 모델 안에 associate 으로 정의가 되어 있어야 했다.

 

내가 필요한 정보는 Timetable 에서 Lecture 과 연결되어 있는 것으로 만들 수 있다고 판단하고, TimeTable - Lecture 을 연결하여 데이터를 불러올 수 있었다.

Timetable.count({
  include: {
    model: Lecture,
    where: {
      id: lec.id,
    },
  },
  distinct: true,
  col: 'userId',
})

약간의 시행 차고를 통해서 완성할 수 있는 코드였는데, 많이 보였던 애러가, 필요한 필드의 위치가 다른 테이블로 선택이 되어 필드를 찾을 수 없다는 오류였다. 내가 오류를 해결했다기 보다, 여러가지를 해보면서 자연스럽게 해결된 케이스라, 다음에 다른 프로젝트를 통해 직접 해보면서 문제가 발생하면 그 때 노하우를 쌓아야 할 것 같다.

exports.getSearchResults = async (req, res) => {
  const { search, page } = req.query;
  const limit = +process.env.PAGE_LIMIT;
  const decodedSearch = decodeURIComponent(search);

  Search.create({ userId: req.user.id, search: decodedSearch });
  const { count, rows: lectures } = await Lecture.findAndCountAll({
    where: searchWhereClause(decodedSearch),
    limit,
    offset: page ? limit * (+page - 1) : 0,
  });

  // Get spike information
  const lecturesWithCount = await Promise.all(
    lectures.map(
      async (lec) =>
        new Promise(async (res) => {
          const [add, bookmark, spike] = await Promise.all([
            // Add
            Timetable.count({
              include: {
                model: Lecture,
                where: {
                  id: lec.id,
                },
              },
              distinct: true,
              col: 'userId',
            }),
            // Bookmark
            UserLectureRelation.count({
              where: { lectureId: lec.id },
            }),
            // Spike
            UserLectureGleaningRelation.count({
              where: { lectureId: lec.id },
            }),
          ]);

          return res({
            ...lec.dataValues,
            count: {
              add,
              bookmark,
              spike,
            },
          });
        }),
    ),
  );

  res.send({ pages: count === 0 ? 0 : Math.ceil(count / limit), lectures: lecturesWithCount });
};

 

 

추가/즐겨찾기/이삭줍기 카운트 백엔드 by junglesub · Pull Request #53 · zoomkoding/college-timetable

#48 기능 백엔드 기능입니다. 한번도 sequelize 라이브러리를 사용해 본적이 없어 기존 @zoomkoding 님의 코드를 보고 구글링을 하면서 최대한 따라해 보았습니다. 많이 부족한데, 더 좋은 방법이나 다

github.com

 

댓글