📚

리다이어리(Readiary)

2023. 09. — 2023. 12.

정보

이 글은 회고 성격의 글이에요. 프로젝트에 대한 다른 정보들은 다음 링크를 이용해주세요.

컴퓨터과학 종합설계를 수강하며 진행한 프로젝트입니다.

목차

학부 졸업을 앞두고 종합설계 과목을 수강하게 되었다. 별도의 강의 내용은 없고, 종합설계를 진행하며 매주 진행 상황과 성과를 발표하고 피드백 받는 과정이 주가 되는 과목이었다. 한편, 우리 팀은 4명으로 구성된 팀이었고, 실제 서비스 개발에서는 웹 프런트엔드 개발을 주로 담당하게 되었다.

주제 선정

평소 독서 기록을 남길 때 기존 앱들은 여러모로 아쉬운 부분이 많았다. 그래서 나는 이번 기회에 독서 관련 앱을 제작해서 독서 기록을 남기기 좋은 서비스를 만들면 좋을 것 같다고 의견을 제시했고, 팀원들과 충분한 상의 끝에 독서 관련 주제로 가는 게 좋을 것 같다는 의견을 받았다.

처음 우리 팀이 정한 주제는 단순한 ‘독서 기록장’에 가까웠다. 하지만 (시간만 들이면 할 수 있는) 기본적인 기능만으로는 부족하다는 피드백을 받고, ‘독서 기록’보다도 ‘독서’ 자체를 돕는 서비스를 기획하기 위해 애썼다. 결과적으로 생성형 AI(GPT, Stable Diffusion)를 활용해 독후 활동을 도와주는 방향으로 결정했다.

독서록?

처음엔 책의 내용을 바탕으로 제작하면 좋을 것이라 생각했지만, 현실적으로 책의 내용은 접근하기 어려운 부분이 많아(저작권, 비용 등) 다른 방법이 필요했다. 우리의 결정은 ‘스크랩’이었다. 그렇다면 이 스크랩을 활용해 무엇을 할 수 있을까.

처음엔 독서록을 생성하는 데 도움을 주면 더 쉬운 독서가 가능하지 않을까 생각했다. 하지만 이 부분은 독서록을 작성하는 행위 자체를 대체한다는 점에서 필요성에 의문이 들었다. 그래서 나는 아래와 같은 아이디어를 제시했다.

독서록 자체를 생성하는 게 별로라면, 독서록 작성을 위한 화제만을 던져주는 건 어떨까?

그래, 책에 대한 (생각해볼 만한) 질문들을 생성하자!

독서를 하며 책의 일부를 발췌한 스크랩을 기반으로 책에 대한 (생각해볼 만한) 질문들을 생성한다. 하지만 스크랩은 책에 대한 간접적인 정보이므로 전체적인 질문이 매끄러울 수 있을지 많이 불안했다. 그래서 나는 이 지점에서 다음과 같은 의견을 냈고, 채택되었다.

앞선 문답에 기반해, 질문들을 여러 차례 생성하자!
예컨대, 첫 번째 질문에 대한 답변을 또 다른 기반 데이터로 해 두 번째 질문을 생성하는 식으로.

그림도!

우리 팀은 스크랩을 활용해 다른 독후 활동에 대한 아이디어도 도출했다. ‘스크랩’이라는 것 자체의 특성에 집중했다. 스크랩은 사용자가 감명 깊거나 기억하고 싶은 문장들에 대해 이뤄지는 경우가 많다. 그래서, 이런 문장을 시각화할 수 있다면 아주 좋을 것 같다는 의견이 나왔다.

GO!

그렇게 최종적으로 우리 팀이 계획한 주요 기능은 아래와 같다.

  • 스크랩 기반 질문 생성
  • 스크랩 기반 이미지 생성
  • 스크랩 및 독서 기록 관리

생성형 모델 연구

먼저 생성형 모델 쪽을 연구하였다. 팀원 중 인공지능와 그래프 이론 등에 대한 지식이 많은 분이 있어서 내가 생성형 모델 연구를 주도하진 않았다. 하지만 이 과정에 참여한다는 게 좋은 경험이 되었다.

정보
생성형 모델을 활용하는 부분 위주로 논문을 작성해서 제출했다!

개발 (프런트엔드)

나는 이 프로젝트에서 웹 프런트엔드 개발을 맡게 되었다. 특징이 있다면, 모바일에 최적화된 형태로 만들기로 했다. (팀 내외부에서 앱처럼 이용하면 좋겠다는 의견이 많았다. 나는 웹으로 개발하고 싶었기 때문에 앱 개발을 하진 않았다. 어차피 백엔드에 모든 데이터가 저장되고, 기기 자체의 네이티브 기능이 많이 필요하진 않기 때문에 웹으로도 앱과 비슷한 경험이 충분히 가능하다고 설득했다.)

스택

이번 프로젝트는 약간의 내 욕심이 들어갔다. Next.js 13 버전 이상에서 지원되는 App Routing 방식을 십분 활용하고 싶었기 때문이다.

프런트엔드 부분의 개발 스택은 아래와 같다.

  • 언어/프레임워크
    • TypeScript
    • Sass (CSS Modules과 함께 사용)
    • Next.js
  • 주요 라이브러리
    • zustand
    • react-hook-form
    • @tanstack/react-query
    • framer-motion
    • @ducanh2912/next-pwa

개발

독서 기록을 하기에는 모바일 앱이 접근성 측면에서 좋을 것 같다는 팀 내부의 의견이 있었다. 내 생각도 마찬가지였고, 이 때문에 네이티브 앱으로 구현해야 하나 고민이 많이 되었다. 하지만 내가 하고 싶던 건 웹 개발이었다. 그래서 Mobile-first 웹으로 만들어볼까 싶었다. 그래서 간단한 데모들을 보여주며 설득했고, 다행히 웹으로 개발하게 되었다 :)

또 선택할 것이 Next.js를 사용할지의 여부였다. 사실 앱에 가까운 경험은 그냥 SPA로 구현하는 게 맞을 수도 있다. 하지만 나는 Next.js에서 App routing을 새로 지원한다는 점에 눈이 갔다. MPA이지만 공통 레이아웃을 공용해 쓸 수 있다. 이러저러한 점(+ Next.js를 쓰고 싶다는 나의 욕심) 때문에 Next.js를 사용해 구현했다. (물론 App routing만 사용!) 여기에 PWA 지원 정도만 추가하는 게 나의 계획이었다.

스타일링???

스타일링에 대한 고민은 항상 나를 괴롭힌다. pure CSS도, CSS-in-JS도, CSS-in-CSS도 내가 보기엔 깔끔하지 않았다. Tailwind css가 확실히 편하면서도 좋은 접근법인 것 같은데, 동시에 너무 편하기 때문에 스타일링에 대한 독창성이 묻힌다는 느낌이 항상 들었다. 그리고 무엇보다 전 학기에 진행한 시네마서울에서 Tailwind css를 사용하였기 때문에, 이번엔 CSS를 사용하고 싶었다. 그냥 CSS로 하기엔 불편한 점이 너무 많기에… SCSS로 구현하기로 했고, SCSS 변수와 함수를 적극적으로 사용하기로 했다. 이를 위해 내가 작성한 테마 시스템은 아래와 같이 SCSS 함수를 사용했다.

// base/radix-colors.css
@import "@radix-ui/colors/jade.css";
@import "@radix-ui/colors/jade-dark.css";
@import "@radix-ui/colors/sage.css";
@import "@radix-ui/colors/sage-dark.css";

// theme/_color.scss
@function primary($scale) {
  @return var(--jade-#{$scale});
}
@function neutral($scale) {
  @return var(--sage-#{$scale});
}

또, CSS Modules의 composes를 적극적으로 활용했다. 한 컴포넌트에 대해 *.scss*.tsx로 구분되는 만큼 스타일과 마크업이 확실히 구분되어야 한다고 생각했다. 그래서 마크업의 기능과 관련없이 스타일로만 이뤄진 유틸리티 클래스는 모두 composes*.scss 파일에서 다뤘다. 예컨대 아래와 같다.

// base/_ui.scss
@use "theme/color";
.transition {
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
  transition-duration: 150ms;
}
.pressable {
  cursor: pointer;
  @extend .transition;
}

// ~/button.module.scss
.root {
  composes: pressable from global;
  ...
  &__inner {
    ...
  }
}

Fetching을 어디서 할 것인가

Server Component를 활용하다보니 백엔드로의 API Fetching을 어떻게 처리해야 할지 난감했다. 모든 Fetching을 Client에서 할 것인가, Server에서 할 것인가..

처음엔 Client와 Server에서의 Fetching을 모두 혼용해서 사용했다. 로그인 관련해서도 생각할 거리가 있었다.

  • Client에서 Fetching하는 경우 -> JWT 토큰을 저장하고 있어야 함(Web Storage 또는 Zustand와 같은 전역 상태로 관리 必)
  • Server에서 Fetching하는 경우 -> JWT 토큰을 Cookie에 저장하고 있어야 함

처음엔 두 곳에서 모두 이용하기 위해 httpOnly Cookiezustand state 두 곳에 JWT를 저장했다. 물론 이 두 정보를 동기화해주는 로직도 작성해놓았다.

그러던 중…

Server Action

모두 Server Action으로 통합해버렸다. Server Action 내의 코드는 Server에서 동작하기 때문에 JWT 토큰은 httpOnly Cookie에만 저장해줬다. 비교적 신기능이라 (문서가 많지 않아) 도입을 망설였지만, 시험적으로 사용해보니 충분히 좋은 것 같았다.

사실 모두 서버 측에서 처리하기 어렵다고 판단했던 게 바로 API Route를 일일히 다시 짜준다는 게, 이미 백엔드를 별도로 개발하는 상황에서 또 다른 백엔드 작업을 하는 느낌이었던 게 컸다. 하지만 Server Action은 Client와 Server 사이의 요청을 상당히 자연스럽게 연결해준다. 어차피 클라이언트에서만 처리해도 API 요청만을 담는 코드를 따로 작성해야 하는데, 이런 느낌으로 서버 측 요청을 구현할 수 있었다!

단점이라면.. (아직까지는) 클라이언트 호출에 비해 디버깅하기 어려웠다.

또한 pure object만 주고받을 수 있기(이건 Server Component와 Client Component 사이에서도 마찬가지) 백엔드 요청에서의 오류를 Client로 보내기 전 처리해야 한다. 이건 어떻게 보면 Server Action도 결국 HTTP 통신이라는 걸 생각하면 수긍되는 부분이다. 예외 처리 부분은 사실 백엔드 파트와의 얘기도 덜 됐고, 시간 문제로 제대로 하지 못했는데 이 부분이 아쉽다..

아직 낯선 App 구조

파일이 너무 많아진다는 점에서 아직 구조를 짤 때 낯선 부분이 많았다. 또 같은 화면을 구성하면서도 Server Component를 사용하기 위해서 최대한 작은 부분만 Client Component로 짜자는 생각에 과도하게 많은 파일을 생성했다. 내가 보기에 점점 과해졌고, 이 지점에서 고민을 많이 했다. 결국, Client Component를 사용하는 데 너무 경계를 가지는 건 좋지 않다는 판단이 내려졌다. 어차피 Client Component라고 해도 서버에서 렌더링은 된다.

Server Component 사용의 의의

그렇다면 Server Component를 사용하는 의의는 무엇일까? 흔히 이름 때문에 Client는 CSR이고 Server는 SSR일 거라 착각하기 쉽다. 이는 완전히 잘못되었다. Client Component도 기본적으로 SSR로 구현된다! 쉽게 말해, Server에서만 실행되는 게 Server Component, Client에서도 실행되는 게 Client Component이다.

관련 내용은 별도로 포스팅하였다.

논문을 제출하다

한국정보과학회 KSC2023에서 진행되는 학부생 논문 경진대회에 이번 프로젝트 진행 내용에 대한 논문을 작성해 제출했다. 이후 부산 벡스코에서 열린 KSC2023 컨퍼런스에 참가해 포스터 발표를 진행했다! (나는 발표자는 아니었다)

회고

아쉬운 점

사실 결과물의 퀄리티에서 아쉬운 점이 꽤 있다. 기획부터 개발까지 마치기엔 비교적 짧은 기간인 15주 안에 모든 걸 마쳐야 했고, 그 과정에서 논문 작성, 연구 참여를 하다보니 본격적으로 개발을 한 건 3-4주 정도였다. 더 퀄리티 높은 코드를 작성하고 싶었지만 시간적인 한계가 매우 크게 느껴졌다.

그래도 다행인 건 팀 내외에서 모두 퀄리티랑 디테일이 대단하다는 조금은 다행인 평가가 많았다 :)

배운 것

AI를 활용한 서비스 개발 경험을 쌓았다는 점이 가장 크게 배운 점 아닐까 싶다. AI는커녕 파이썬조차 잘 알지 못하기 때문에 인공지능이 최근 최고의 화두임에도 나는 그냥 개발만 했다. 그렇지만 관심은 있었는데, 아예 접근하기 어려웠다.. 이번 기회에 이쪽 분야와 관련있는 분들을 만나고 같이 협업하며 많은 걸 배우게 되었다. 전통적인 인공지능 모델을 손댄 것은 아니지만, GPT를 비롯한 생성형 AI를 활용하는 방법(프롬프트 튜닝, 결과 평가 방법)을 배우게 되었다.

프런트엔드 개발자로서는, Next.js의 Server Component를 적극적으로 활용했다는 점에서 큰 배움을 얻었다. Server Component는 서버에서 완전히 실행되기 때문에 Client Component와 매우 많이 달랐던 것 같다. 동시에 비교적 최신 기능이기 때문에 버그가 많았는데, 이를 트러블슈팅하는 과정에서 뜻하지 않게 Next.js의 생태계에 대해 더 깊이 알게 되었다. 동시에 Next.js 같이 규모가 큰 프레임워크에도 꽤나 치명적인 버그들이 많이 있다는 걸 깨달으며, 다시 한 번 트러블슈팅의 중요성과 어려움을 알게 되었다.

동시에 짧은 시간에 빠르게 개발하며 시간 조절하는 방법을 많이 배운 것 같다. 나는 디테일에 다소 집착한다. 이게 장점이기도 하지만 단점이기도 하다. 특히 프런트엔드 개발은 공정 상 개발의 뒤에 오는 경우가 많아 항상 시간에 쫓긴다. 하지만 이번 프로젝트에서는 일부러 계획적으로 기능들을 개발했다. 하나의 기능을 완벽히 하는 데 몰두해 다른 기능들을 쫓기듯 만드는 상황을 피했다. 그럼에도 저번 학기에 시간 쫓기며 개발했던 시네마서울 개발과 비교해 평가가 나쁘지 않았다 :)