Veltrends는 개발, IT, 디자인, 스타트업 관련 유익하고 재미있는 소식들을 한 눈에 볼 수 있는 서비스입니다.

홈페이지: https://www.veltrends.com/
GitHub Repo: https://github.com/velopert/veltrends

이 포스트에서는 이 프로젝트를 만들면서 했던 생각들을 여러분들께 공유해보고자 합니다.

프로젝트를 만든 이유

이 프로젝트를 시작한 주요 목적은 기존에 제가 집필했던 『리액트를 다루는 기술』, 『리액트 네이티브를 다루는 기술』 에서 다루는 마지막 프로젝트 개발 튜토리얼을 대체하는 것이였습니다. 그래서 최대한 복잡하지 않고 이해하기 쉬운 코드로 작성을 해야겠다는 생각을 가지고 시작을 했습니다. 하지만 만들다보니, 이왕 만드는거 조금 더 진심으로 만들어보자라는 마음을 갖게 됐고, 서비스를 충분히 활성화 시킬 수 있지 않을까 싶어서 프로덕션 배포까지 진행을 하게 되었습니다.

열심히 개발을 하다 보니 프로젝트의 결국 좀 복잡해지긴 했고.. 입문자가 그대로 따라 할 수 있는 수준을 조금 벗어나게 되었습니다. 나중에 책에 실은 프로젝트의 경우엔 Veltrends Lite 버전을 만들 계획입니다.

이 프로젝트는 해외의 Hackernews와 국내의 GeekNews를 모티브로 만들어졌습니다.

이 프로젝트는 최대한 이미 알고 있는 기술을 사용해서 빠르고 쉽게 만드는게 의도였지만 프로젝트 개발을 하다보니 이번 프로젝트를 통해 새로운 것들을 꽤나 많이 경험하게 되었습니다. 중요하다고 생각하는것들 순으로 정리를 해보겠습니다.

1. 모바일 우선 디자인 (Mobile First Design)

지금까지 서비스 개발을 해오면서, 매번 데스크탑 우선 디자인(Desktop First Design)을 해왔었습니다. 즉, 넓은 화면에서 보여지는 UI/UX 기획을 한 다음에 나중에 모바일 화면에 최적화를 해주는 것이죠. 이러한 방식은 기획 과정에 충분한 시간을 들이지 않으면 모바일 디바이스에서의 UX를 챙기기가 참 어려운 것 같습니다. 넓은 화면에서 보여주던걸 좁은 화면에서 보여주기 위해서 숨길 것들을 숨기고 재배치하거나 새로 만드는 작업이 많이 요구되기 마련이기 때문입니다.

저는 특히 프로젝트를 만들 때 모바일 호환 작업을 가장 나중에 하는 습관이 있는데요, 빨리 배포하고 마무리하고 싶다 보니까 모바일 호환 작업에 공수를 덜 들이게 되고 그러다보니 모바일에서의 UX가 좋지 않은 경우가 많았습니다.

모바일 우선 디자인(Mobile First Design)에 대해서는 이전부터 많이 들어왔었는데, 이번 프로젝트에 처음으로 도입을 해보게 되었습니다. 이 방식은 기획 과정에서 모바일 화면을 우선 만들고 나중에 더 넓은 화면으로 확장해나가는 방식입니다. 이 방식의 가장 큰 장점은 모바일 화면에서의 UX를 먼저 고려하면서 기획을 할 수 있다는 것입니다.

이번 프로젝트에서 모바일 우선 디자인을 했었기에, 모바일 디바이스에서 하단 탭을 보여줄 생각을 할 수 있었습니다.

만약 데스크탑을 먼저 기획했었더라면 해당 기능들을 헤더에다가 다 몰아놨다가 모바일에서는 햄버거 메뉴를 만들었겠죠. 또는, 사용자 로그인 정보를 눌렀을 때 보여줄수도 있었을 것 같습니다.

그렇게 메뉴가 숨겨져있다면 사용자는 이 서비스에 어떤 기능들이 있는지 파악하기 어려울 것 입니다. 주요 기능들을 바로 노출시켜줌으로써 사용자가 서비스에 대해서 학습하는 리소스를 최소화시킬 수 있습니다.

특히, 헤더에 너무 많은 항목들을 넣으려면 디자인적으로도 매우 복잡했을 것 입니다.

위 이미지는 데스크탑에서 보여지는 헤더입니다. 만약 데스크탑에서 하던 대로 모바일 헤더에서도 새 글 작성, 검색, 로그인/로그아웃 등의 기능을 넣으려고 하면 자리가 충분하지 않았겠죠.

추가적으로, 모바일 우선 디자인을 하니까 서비스 기능의 핵심에 대해서 좀 더 집중할 수 있던 점이 참 좋았습니다. 기획 단계에서부터 사용할 수 있는 화면의 크기가 제한되어 있기 때문에, 자연스레 불필요한 기능들은 숨기고 핵심 기능들만 노출시킬 수 있게 되었습니다. 그러다보니 더 쉬운 사용자 경험을 제공할 수 있도록 유도되었습니다.

그리고, 더 넓은 화면으로 확장해나가는건 난이도가 꽤 쉬웠습니다. 보통 넓은 화면 우선 디자인을 한 다음에 나중에가서 모바일 디바이스 호환을 하다 보면 대응이 덜 된 UI가 있다면, UI가 화면에서 잘릴수도 있어서 버그처럼 보이게됩니다. 반면, 반대의 상황의 경우엔, 모바일 UI가 데스크탑에서 뜨게 됐을 땐 비록 덜 편할 수 있긴 하지만 버그처럼 느껴지지 않습니다. 따라서, 좀 더 여유롭게 대응할 수 있습니다. 쉽게 정리하자면 데스크탑 -> 모바일 대응은 화면이 잘리지 않도록 대응하는 느낌인 반면에, 모바일 -> 데스크탑 대응은 어떻게 하면 데스크탑에서 좀 더 편하게 보여줄 수 있을지 고민하여 개선하는 느낌입니다.

예를 들어서 댓글을 수정하거나 삭제할 때 모바일에서는 하단 모달을 띄웠었습니다. 이러한 UX를 똑같이 데스크탑에서 사용하면 어색한감이 있긴 하죠. 초반엔 단순히 최대 너비를 정해서 보여주도록 배포를 한 다음에 나중에가서 popper UI가 나타나도록 개선을 해주었습니다.

이렇게 모바일 우선 디자인을 하면서 배운 점이 많았고, 데스크탑 사용자가 압도적으로 많은 서비스가 아닌 이상 앞으로 저는 가급적이면 계속해서 모바일 우선 디자인을 하게 될 것 같습니다.

2. Remix 후기

이번 프로젝트에선 제가 처음으로 Remix를 프로덕션에서 사용을 해보았습니다. 충분히 재밌고 편한 프레임워크였지만 불편한 것들도 있었습니다.

초심자에게 추천하기엔 아직 이르다

사용해보고 느낀점은 초심자들에게 사용하라고 권장하긴 아직 이르다는 생각이 들었습니다.

Remix가 익숙하지 않은 독자들을 위해서 간단하게 설명을 해보자면, 리액트로 풀 스택 웹 사이트를 만들 수 있게 해주는 웹 프레임워크입니다. 쉽게 생각하면 Next.js 처럼 서버사이드 렌더링, 코드 스플리팅, 라우팅 등을 지원해주는 프레임워크라고 생각하시면 됩니다. 좀 더 나아가자면 Remix는 풀 스택 프로젝트를 만드는 것에 초점이 좀 더 많이 잡혀있어서 프로젝트에서 데이터베이스에 직접 접근하거나 인증을 처리하는 등의 작업을 할 수 있습니다. 특장점중에 하나는 React Router를 만든 개발자들이 만든 프레임워크이기 때문에 React Router와 API 호환이 아주 잘 되지요.

Remix가 추구하는 개발 방식은 꽤나 신선합니다. 백엔드와 프런트엔드를 분리하지 않고 한 프로젝트로 관리를 할 수 있게 해주지만 과거의 PHP 같은 고전의 방식이 아닌 좀 더 모던한 방식으로 이를 가능하게 해주니까요. 만약에 이렇게 백엔드와 프런트엔드를 함께 작성하고 싶다면 Remix는 참 이상적인 선택인것이 확실하지만, 저 같은 경우는 백엔드와 프런트엔드가 분리되어있긴 했습니다.

백엔드와 프런트엔드가 분리되어있는 프로젝트에서도 Remix를 사용할 수 있습니다. Database를 접근하는 대신에 API Client로 API를 호출해주면 됩니다. 이론상 정말 간단합니다.

import { json, LoaderFunction } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { getItems } from '~/lib/api/items'
import { GetItemsResult } from '~/lib/api/types'

export const loader: LoaderFunction = async () => {
  const items = await getItems({ mode: 'trending' })
  return json(items) // 여기서 반환된 값을
}

function Home() {
  const items = useLoaderData<GetItemsResult>() // 여기서 사용 가능
  return <div>{items.list.length}개의 항목</div>
}

export default Home

하지만 문제는 회원 인증상태에 따라 API 결과가 바뀔 수 있는 경우에 고려해야 할 것이 꽤나 많습니다.

  1. API Client에 쿠키 또는 인증 정보를 언제, 어떻게, 어디서 설정 및 초기화를 할 것인가?
  2. 인증이 필요한 페이지에 접근했을 때 로그인 페이지로 리다이렉트를 어떻게 할 것인가?
  3. 만약 JWT를 사용하는 경우 토큰 리프레쉬 작업은 어떻게 구현할 것인가?

하지만 Remix의 경우 공식문서에서 가이드 하는 내용은 백엔드와 프런트엔드를 함께 작성하는 튜토리얼 뿐이고, 공식 문서에서 다루는 Authentication 관련 예시 또한 풀스택 프로젝트 환경만 다룹니다.

저처럼 API 서버가 따로 분리되어 있는 경우, 참조해야 할 이상적인 가이드는 찾기 힘들어서 결국 제가 길을 직접 찾아가야만 했습니다. 그 과정에서 삽질을 꽤나 많이 했고 시간도 꽤 많이 소비했습니다.

결국 해결할 수 있는 문제이긴 하지만 그 과정이 꽤나 어려웠던 것 같습니다. 그래서 이 프레임워크를 초보자에게 권하기엔 아직은 조금 무리가 있지 않을까 하는 생각이 들게 되었습니다.

그래서, 원래는 Remix에 관련된 내용을 『리액트를 다루는 기술』 에 실을 예정이였으나, 이번에 이렇게 사용해보고 책 내용에서 빼기로 결정하였습니다. 만약에 Remix에 대한 레퍼런스 자료가 충분히 더 많아지고 특히 공식 문서에서도 API 서버가 분리되어 있는 상황에 대해서도 충분한 가이드가 제공된다면, Remix를 추천할 수 있게 될 것 같습니다.

action은 좀 불편하다

Remix는 사용자가 서버에 데이터를 전달할 때 (예: 로그인, 글쓰기, 댓글 작성 등) action이라는 개념을 사용합니다. 컴포넌트에서 Form Submit이 일어나거나, 별도의 함수를 따로 호출하였을 때 Remix 서버에서 데이터를 전달받아서 원하는 로직을 수행한 뒤 그 결과를 클라이언트에서 받아서 사용할 수 있게 해줍니다. 관련 내용은 Data Writes에서 확인할 수 있습니다.

저는 처음에 이 기능을 잘 활용해보려고 했었으나 사용할수록 좀 불편했습니다. 만약 이 Remix 프로젝트가 풀스택 프로젝트였다면 편했을지 모르겠지만 이미 제 프로젝트는 프런트엔드와 백엔드가 분리되어 있는 상태고, 제가 action을 작성하게 된다면 단순히 데이터 전달 받아서 백엔드에 요청해서 데이터 받아와주는 프록시같은 역할만 하기 때문에 이게 무슨 의미가 있나 싶었습니다. 작성해야 하는 코드가 더 늘어나기만 했구요.

추가적으로, 한 페이지에서 발생할 수 있는 데이터 쓰기가 다양하다면, (예: 링크 상세 페이지에선 좋아요, 북마크, 댓글 작성, 댓글 좋아요 등 여러 액션이 있음) 이에 대한 로직 처리가 굉장히 까다로워집니다. action 함수에서 if문이나 switch문을 사용해서 각 상황에 따라 처리를 해줘야 하는데.. 별로였습니다.

action을 잘 활용하면 좋은 한가지 장점은 JavaScript가 작동하지 않아도 서비스가 문제없이 작동한다는 것 입니다. 그런데, 제 서비스의 사용자 중에서 JavaScript를 꺼놓고 사용하는 사람도 없고, JavaScript 리소스 로딩도 금방 되기 때문에 이 장점은 큰 의미가 없었습니다.

그래서 결국엔 개발 후반부에는 API 서버에서 CORS 설정을 해놓고 Tanstack Query를 하였습니다.

데이터 로딩의 경우에도 서버 사이드 렌더링을 하기 위해 초기 데이터는 loader를 사용하도록 하고 데이터 최신화 또한 Tanstack Query를 활용했습니다.

배포는 Vercel로 하는게 좋다

저는 이 서비스를 초반에 Cloudflare Pages에 배포를 했었습니다. Cloudflare Pages는 무료이고, 추후 사용량이 많아져서 트래픽이 많아져도 계속 무료로 쓸 수 있다는 점이 좋았습니다. 하지만 3가지 단점들이 있었습니다.

  1. 프로젝트를 Cloudflare Pages 로 배포할 수 있도록 설정을 하면 HMR이 너무 느리다. (코드 수정할 때 마다 개발 서버가 꺼지고 다시 켜지는데 7~8초 정도 걸림)
  2. Edge Function에 대한 로그를 볼 수 없어서 디버깅이 어렵다.
  3. 한국 리전을 지원하지 않아서 일본망을 타기 때문에 느릴 수 있다.

1번 문제가 너무 치명적이였습니다. 코드 수정할때마다 7~8초 기다려야되는건 너무 불편했습니다. 그래서 Vercel로 넘어가기로 결정을 했습니다.

반면 3번 문제도 Vercel로 옮기는 중요한 요인이 되기는 했었지만 도메인 연결 후 자동으로 활성화되는 Cloudflare CDN을 비활성화하면 한국망을 사용할 수 있다는 점을 Vercel 마이그레이션 이후에 알게 되었습니다.

나중에 Remix + Cloudflare Pages의 DX가 좀 더 개선이 된다면 Cloudflare를 한번 더 고려하게 될 것 같습니다.

Remix 좋았던 점

Remix를 사용하면서 특별히 좋았던 점들을 꼽아보자면 다음과 같습니다.

  1. React Router와 동일한 API
    저는 Router API는 Next.js 보다 React Router의 것을 좀 더 선호하는데 API가 동일해서 참 편했었습니다. 특히 Outlet을 사용할 수 있다는 점이 좋았습니다. Next.js도 유사한 기능이 개발이 진행중이며 조만간 릴리스 될 예정이긴 하지만 아직은 릴리스되지 않았습니다.
  2. prefetch 기능
    Link 컴포넌트에 prefetch를 활성화하면 해당 페이지에서 필요한 리소스들을 미리불러와줍니다. JS/CSS 리소스 뿐만 아니라 loader에 정의한 데이터도 미리 불러와줍니다. 그리고, Cache-Control 헤더를 통해 페이지별로 캐싱 정책을 설정할 수 있습니다. 현재 서비스에서는 홈에서 링크에 마우스를 올리면 데이터를 미리 불러옵니다. 대부분의 경우 커서를 이동하고 클릭을 하는 사이 데이터 로딩이 끝나서 사용자가 느끼기엔 로딩 없이 바로바로 열리는 것 처럼 느껴질 것 입니다 (물론 서비스 규모가 더 커지고 데이터가 더 많아지면 최적화를 잘 해주어야 이러한 UX를 계속 유지할 수 있을 것입니다). Next.js의 경우엔 현재 데이터까지 미리 불러오려면 swr이나 Tanstack Query 같은 라이브러리의 도움을 받아야합니다.
  3. loader 이해하기가 쉬움
    loader에서 원하는 데이터를 반환하고 useLoaderData Hook으로 페이지에서 그 데이터를 받아와서 사용한다는 점이 매우 쉽고 명료하게 느껴졌습니다. Next.js의 경우엔 데이터 로딩의 경우 getInitialProps, getServerSideProps, getStaticProps 이렇게 다양하죠. 물론 복잡한 개념들은 아니긴 하지만 처음 접하는 사람들에겐 loader 하나만 작성하는게 확실히 헷갈리지 않고 쉬운 것 같습니다.

Remix vs Next.js 뭐가 더 좋냐?

상황에 따라 다른 선택을 하게 될 것 같습니다. 만약 기존에 React Router로 만든 프로젝트가 있다면 Remix로 전환이 꽤 쉽기 때문에 좋은 선택지라고 생각합니다.

SSG(Static Site Generation)가 필요하다면 Next.js가 더 좋은 선택지라고 생각합니다. Remix에서는 SSG를 지원하지 앉히만 캐싱을 통해 SSG급으로 빠른 페이지 로딩을 가능케 할 수 있습니다. 하지만, Next.js의 SSG는 단순 정적 파일 호스팅 만으로도 웹사이트를 제공할 수 있는 반면 Remix는 서버 사이드 렌더링을 해 줄 컴퓨팅 자원(Edge Function, Lambda, 등)이 필요합니다.

Remix의 공식 문서가 가이드하는대로 풀스택 프로젝트를 만든다면 Remix를 선택하는게 무조건 좋긴 하겠지만, Frontend 와 Backend가 분리되어 있다면, Next.js를 선택하는게 아직까진 더 쉬운 선택지인 것 같습니다. 개발하는 과정에서 참고할 수 있는 자료가 훨씬 많으니까요.

추가적으로, 주변에 Next.js를 써본 사람은 엄청 많은데 Remix를 써본 사람은 꽤 적은 편입니다. 특히 이걸 사용하는 회사는 국내에서는 찾기 정말 힘들죠. Kent의 블로그에 따르면, 해외에서는 Netflix, Microsoft, Tesla 등의 회사에서도 부분적으로 이미 Remix를 사용하고 있다고 합니다. 한 2~3년 뒤면 국내에서도 Remix 사용 사례가 많이 나오지 않을까 싶습니다. Remix가 뭐 특별히 어려운 것도 아니고 금방 배울 수 있긴 하지만, Next.js가 익숙한 개발자들이 한국에 훨씬 많기 때문에, 회사 프로젝트에서는 Next.js를 선택하는게 아직까지는 더 좋은 선택지라고 생각합니다.

저는 앞으로 제가 관여할 수 있는 프로젝트라면 Remix를 또 사용하게 될 것 같습니다. 하지만, 누군가에게 추천을 한다면 아직까지는 Next.js를 권장할 것 같습니다.

3. Terraform

Terraform은 인프라 관리를 코드로 할 수 있게 해주는 기술입니다. 보통 서비스 배포를 할 때, AWS를 사용한다면 AWS 대시보드에서 이것 저것 수작업으로 직접 많이 하게 됩니다. 어려운 일은 아니지만 나중에 다시 하려고 하면 헷갈리고 다른 사람에게 설명해야하거나 인수인계를 해야 하는 과정에서 매우 번거롭고 실수 할 가능성이 많아집니다.

저는 이번에 API 서버를 ECS로 배포하고, 서비스 이용량에 따라 자동으로 확장이 가능하도록 Auto Scaling도 적용을 하고 싶었습니다. AWS에서 ECS로 서비스를 배포하려면, 꽤나 손이 많이 갑니다.

  1. Docker Image 만들어서 ECR에 등록
  2. ECS 작업 정의
  3. ECS 클러스터 생성
  4. ECS 클러스터에 서비스 생성
  5. Auto Scaling 설정
  6. 로드 밸런서 설정
  7. 보안 그룹 설정

과거에 이 작업을 수작업으로 한 적이 있었는데 새로 배포 설정을 할 때 마다 할 일이 많았고, 실수를 했을 때 어디에서 실수를 했는지 파악하기가 어려웠습니다. 그래서 이번에 ECS를 사용해보았는데 매우 만족스러웠습니다.

물론, 그냥 직접 수작업으로 직접 배포를 했다면 1~2시간이면 끝날 일을 Terraform 학습까지 하고 삽질을 하느라고 5시간은 넘게 걸렸던 것 같습니다. 그래도 덕분에 이번 기회에 Terraform을 배워볼 수 있었고 미래의 서비스 배포는 보다 빠르게 배포할 수 있게 될 것 같습니다.

Terraform을 사용하면 직접해야 하는 작업을 명령어 한방으로 끝낼 수 있게 해줍니다. 그리고 변경사항이 필요하면 코드를 수정하고 다시 또 명령어를 입력하여 반영을 시킬 수 있죠. 이렇게 하면 배포 과정에서 실수를 줄일 수 있고, 배포 과정을 자동화 할 수 있습니다.

최근에 트위터에서 Pulumi 라는 기술을 추천받았었는데 다음 프로젝트에서는 이걸 활용해보지 않을까 싶습니다. Terraform은 환경설정 파일을 코드로 작성하는 느낌이라면, Pulumi는 TypeScript, Python 등 우리가 익숙한 프로그래밍 언어로 배포 로직을 작성해내는 느낌입니다. 다음번에 사용해보고 한번 후기를 남겨보도록 하겠습니다.

4. Fastify

Fastify를 택한 이유

백엔드는 Node.js와 Fastify 프레임워크를 사용했습니다. Node.js와 TypeScript를 쓸 때 일반적으로 Nest.js 를 진짜 많이 쓰는데, 저는 Nest.js를 취향적인 문제로 그리 좋아하지 않습니다. Nest.js 에서 정해놓은 설계를 무조간 따라야하고 제대로 활용하기 위해선 Nest.js 프레임워크에서 제공되는 기능들과 사용하는 개념들에 대해서 일일이 공부해야합니다.

물론 훌륭한 프레임워크라고 생각은 합니다. 많이 연구를 하지 않아도 아주 튼튼한 설계를 가지고 안정적인 백엔드를 구성할 수 있기 때문이죠. 그러나 저는 그런 설계를 따라가기보다는, 제가 원하는 방향으로 설계를 하는 것을 추구합니다.

저는 백엔드 개발을 할 때는 딱 원하는 엔드포인트로 요청을 받으면 그에 맞는 데이터를 반환해주는 방식으로 구현을 하는 것을 선호합니다. Express, Koa, Flask, FastAPI, Gin 처럼 말이죠. 비슷한 이유로 Spring이나 Django를 별로 좋아하지 않습니다. 그런 프레임워크를 사용하다보면 나는 당장 내가 구현할 비지니스 로직에 집중하고 싶은데 해당 프레임워크의 공식 문서를 더 많이 봐야 하는 경우가 많아서 별로 좋아하지 않았습니다. 물론 충분히 숙련이 됐다면 공식 문서를 참고하는 빈도는 줄어들겠지만 말이죠.

Express는 정말 많이 사용되지만 이를 사용하는 프로젝트가 엄청 많은 것일 뿐, 커다란 개선사항 없이 유지보수만 되고 있는 프레임워크이기 때문에 새로운 프로젝트에서 사용하는 것은 별로 좋은 선택지는 아닌 것 같습니다.

Koa도 마찬가지로 Express의 단점을 보완하기 위해 만들어졌지만 이 프로젝트 또한 프레임워크 제작자인 TJ가 Golang 진영으로 떠나면서 유지보수만 되고 있고 커뮤니티가 이를 유지보수하고 있다보니 프로젝트의 방향성을 잃었습니다.

이러한 상황에서 저는 Fastify가 가장 좋은 선택지라고 생각합니다. 안정적이고, 계속해서 발전하고, DX에 대해서 신경을 쓰고, TypeScript와 최신 Node.js와 호환이 잘 되기 때문입니다.

Fastify는 정말 훌륭하지만 잘 못하고 있다는 점이 있다면 입문자를 위한 가이드가 부족하다는 것 이라고 생각합니다. 솔직히 진짜 좋은 프레임워크인데 초보자의 눈으로 본다면 배울 때 어디서부터 어떻게 시작하고 또 어떻게 해야 잘 쓰는건지 터득하기 어렵다고 생각합니다. 반면 Node.js 사용 경험이 있으신 분들은 다들 알아서 잘 쓰시고 계신 것 같습니다.

JSON Schema와 TypeScript

저는 과거에도 Fastify를 썼던 적이 있었는데 그 때는 JSON Schema와 TypeScript 의 Type을 준비하는 과정이 좀 번거로웠었습니다. 그 때는 JSON Schema를 직접 작성하고, 이를 Type으로 변환시켜주는 json-schema-to-ts라는 라이브러리를 사용했었는데 스키마 파일이 여기저기 흩어져있고 라우트에서 JSON Schema랑 타입을 하나하나 불러오는게 번거로웠습니다.

그 때의 코드를 잠깐 예시로 들어보겠습니다.

import GetHistoricalPricesParamsSchema from "schema/assets/getHistoricalPrices/params.json";
import GetHistoricalPricesQuerystringSchema from "schema/assets/getHistoricalPrices/querystring.json";
import { GetHistoricalPricesParams } from "types/assets/getHistoricalPrices/params";
import { GetHistoricalPricesQuerystring } from "types/assets/getHistoricalPrices/querystring";

fastify.get<{
  Params: GetHistoricalPricesParams;
  Querystring: GetHistoricalPricesQuerystring;
}>(
  "/:ticker/historical-prices",
  {
    schema: {
      params: GetHistoricalPricesParamsSchema,
      querystring: GetHistoricalPricesQuerystringSchema,
    },
  },
  async (request, reply) => {
    // request.params 와 request.querystring의 타입이 추론됨
    // (...)
  }
);

이렇게 Schema타입을 설정하고 나면 API 요청이 들어왔을 때 잘못된 형식으로 데이터가 요청되면 적당한 400 Error를 발생시키고, 데이터 형식이 제대로 됐다면 API 로직 함수에서 request.paramsrequest.querystring의 타입이 추론됩니다. 근데 이게 되게 하기 위해서 준비해야 하는 코드가 너무 많았죠. schema도 설정해주고, Generic도 설정을 해주어야했습니다.

이번에 작성한 프로젝트에서는 @fastify/type-provider-typebox 라는 것을 사용했습니다. 이 라이브러리는 TypeBox를 사용하여 JSON Schema를 생성하고, 이를 스키마로 등록했을 때 Generic으로 타입으로 등록하지 않아도 자동으로 타입을 추론해줍니다.

다음은 예시 코드입니다.

import { Type } from "@sinclair/typebox";
import { createAppErrorSchema } from "../../../lib/AppError.js";
import { routeSchema } from "../../../lib/routeSchema.js";

export const AuthBody = Type.Object({
  username: Type.String(),
  password: Type.String(),
});

const TokensSchema = Type.Object({
  accessToken: Type.String(),
  refreshToken: Type.String(),
});

const AuthResult = Type.Object({
  tokens: TokensSchema,
  user: UserSchema,
});

export const loginSchema = routeSchema({
  tags: ["auth"],
  body: AuthBody,
  response: {
    200: AuthResult,
    401: createAppErrorSchema("WrongCredentials"),
  },
});
import { loginSchema } from "./schema.js";

fastify.post("/login", { schema: loginSchema }, async (request, reply) => {
  const authResult = await userService.login(request.body); // 여기서 body 타입 추론 됨
  setTokenCookie(reply, authResult.tokens);
  return authResult;
});

이 라이브러리를 사용하니 JSON Schema와 Typescript를 활용하기 위해서 준비해야 하는 코드들이 많이 줄어들었고 가독성도 높아졌습니다. Typebox를 사용하여 JSON Schema를 작성하는건 직접 작성하는 것 보다 훨씬 편리합니다. 참고로 만약 JSON Schema를 직접 작성한다면 다음과 같은 형식입니다.

{
  "type": "object",
  "properties": {
    "username": {
      "type": "string"
    },
    "password": {
      "type": "string"
    }
  },
  "required": ["username", "password"]
}

추가적으로 API의 반환 값에 대해서도 타입을 검증할 수 있어서 좋았고 API의 스키마를 잘 지정해놓으면 다음과 같이 Swagger UI를 통해 API 문서를 자동으로 생성할 수 있는 점도 참 좋았습니다.

Typebox와 유사한 라이브러리로 zod라는 라이브러리가 있는데요, 이 또한 fastify에서 fastify-type-provider-zod 라이브러리를 사용하여 연동할 수 있습니다.

공식 문서에서는 현재 Typebox만 안내하고 있긴 하지만 zod 관련 라이브러리도 정말 잘 작동하고 편하기 때문에 추천드립니다.

services 디렉터리 분리

이전 프로젝트에서는 각 라우트 핸들러에서 비지니스 로직을 구현하기도 하고, 서비스를 따로 분리하여 라우트가 위치한 디렉터리에 같이 관리하기도 했었는데, 이번에는 서비스를 services 라는 디렉터리에 몰아서 저장을 했습니다.

이렇게 작성을 하니까 비지니스 로직을 각각의 함수로 구현하는 것에 집중할 수 있었고, 서비스에서는 Fastify의 request나 reply의 구현체에 대해서 신경쓰지 않고 구현할 수 있어서 좋았습니다. 서비스가 라우트 핸들러에 종속되어있지 않으니까, 나중에 제가 원하면 언제든지 서버 프레임워크를 쉽게 교체할 수 있고, GraphQL 이나 tRPC 같은 기술이 필요해진다면 더욱 쉽게 적용할 수 있을 것이라 생각합니다.

5. Prisma

이번 프로젝트의 ORM은 Prisma를 사용했습니다. 과거에도 사이드 프로젝트에서 Prisma를 종종 사용해본적이 있는데 이렇게 프로덕션까지 성공적으로 릴리스하는건 이번이 처음입니다.

저는 이 ORM이 매우 만족스럽습니다. 지금까지 Sequelize, TypeORM, Prisma 이렇게 다 써봤는데, Prisma가 개발경험이 참 우수한 것 같습니다.

가장 좋은건 schema.prisma 파일 작성을 통해 데이터베이스 스키마를 정의할 수 있다는 것 입니다. TypeORM이나 Sequelize의 경우엔 각 데이터베이스 테이블을 위한 Entity를 정의해주어야 하는데 Prisma는 스키마 파일 하나에서 관리할 수 있어서 참 좋았습니다. 한 눈에 볼 수 있는 것도 좋고, 특히 테이블 끼리의 Relation 설정을 하는 경우 VS Code의 익스텐션이 자동완성으로 처리해주는 것들이 많아서 편했습니다.

그리고, 타입의 경우도 참 마음에 들었는데, 쿼리를 할 때 특정 필드만 셀렉트하면 해당 필드만 들어있는 타입이 새로 만들어져서 실수할 확률이 줄어들어서 좋았습니다.

저는 이 ORM이 참 마음에 들어서 회사 프로젝트에서 도입하는 것을 고민해봤었고 이미 존재하는 프로젝트에 적용하기도 어렵지 않다는 것을 알게 됐습니다. 특히, 점진적인 전환이 가능했습니다. 회사 프로젝트에서는 기존에 TypeORM을 사용하고 있었는데, 이제 Prisma를 적용하였고 새로 작성하거나 리팩토링하는 API에 대하여 Prisma를 사용하고 있습니다. 아마 벨로그도 올해 안에 Prisma를 적용하게 될 것 같습니다.

TypeORM에서 Prisma로의 전환에 관련된 이야기를 담은 포스트는 6개월 안에 공유해보겠습니다.

백엔드 프로그래밍을 할 때 ORM을 쓰냐 마냐는 갑론을박이 꾸준히 있는 것 같습니다. 개인적으로는 생산성 측면에서는 ORM이 가져다주는 효율이 정말 크다고 생각합니다. 성능 최적화가 필요한 곳에서는 raw SQL을 사용하고 일반적인 상황에는 ORM을 활용하여 개발하면 좋다고 생각합니다.

앞으로 veltrends 계획

서비스가 잘 활성화될 수 있을지는 모르겠습니다. 앞으로 제가 접하는 자료들을 계속해서 큐레이팅하여 벨트렌즈에 올릴 예정입니다. 기능적으로 더 추가하고 싶은 것도 많은데요, 우선은 서비스 활성화에 도움을 줄 수 있는 것들을 우선적으로 구현하게 될 것 같습니다.

아직은 버그도 있고, 개선해야 할 것들도 많습니다. 문의 및 기능 건의는 GitHub Discussions에 넣어주시면 감사드리겠습니다. 오픈소스 프로젝트이고, PR도 환영합니다.

조만간 도입할 기능들은 다음과 같습니다.

  1. 태그 기능
  2. 알림 기능
  3. Cloudflare Turnstile 도입 (스팸봇 방지)
  4. Twitter 봇
  5. Slack 봇
  6. RSS

서비스 활성화가 잘 이뤄진다면 다크모드 도입, 모바일 앱 개발 등을 고려해보고 있습니다. 알림 기능과 Turnstile 도입을 성공적으로 veltrends에 해낸다면 이를 똑같이 벨로그에도 도입을 할 예정입니다.

마치며

사이드프로젝트 개발은 언제나 큰 배움을 가져다주는 것 같습니다. 이번 프로젝트 개발의 목적은 학습이 아니였음에도 불구하고 참 많은 것들을 배웠어요.

한편, 사이드프로젝트를 배포까지 마무리 짓는 것은 참 어렵습니다. 시간을 틈틈이 나눠서 진행하는 것이 참 쉬운 일이 아니죠. 이를 성공적으로 해내기 위한 저의 개인적인 전략은 하루에 딱 1시간만 규칙적으로 하는 것 입니다. 이 전략은 벨로그 개발때도 잘 작동했고, 벨트렌즈에서도 꽤 잘 작동했습니다. 너무 오랫동안 하면 지치게 되고 너무 뜸하게 하게 되면 프로젝트에 대해서 까먹게 되기 때문에 하루에 규칙적으로 1시간씩만 하는게 적당히 흥미를 유지하면서 끈기있게 하기에 좋은 것 같아요. 이번 프로젝트 같은 경우는 하루에 딱 1시간씩 100일동안 작업을 하니까 프로젝트가 마무리 되었어요. 사실은 더 빨리 끝내고 싶었지만, 프로젝트에 정이 가서 좀 더 완벽하게 해내고 싶은 생각에 조금 더 오래 걸리게 되었습니다.

다음 프로젝트는 50시간 안에 배포를 할 수 있는 것을 목표로 해봐야겠습니다. 프로젝트는 빠르게 배포한 다음에 계속해서 개선해내는게 좋다고 생각해요. 왜냐하면 그렇게 해야만 사용자가 무엇을 원하는지 더 일찍 파악할 수 있고, 이를 반영하여 더 좋은 결과물을 만들 수 있기 때문입니다. 최악의 경우엔 서비스가 자리 잡지 못 할수도 있는데 그러면 프로젝트에 들인 시간이 너무나 아까워지기도 하구요.

이 순간에도 사이드 프로젝트를 구상하시거나, 개발 중인 수많은 분들을 응원합니다.

profile
CEO @ Chaf Inc. 사용자들이 좋아하는 프로덕트를 만듭니다.

9개의 댓글

comment-user-thumbnail
2022년 10월 26일

창업도 하시고 개발도 하시고 멋지십니다
잘 되시길 응원할게요! 화이팅!

1개의 답글
comment-user-thumbnail
2022년 10월 27일

👍🏿👍🏿👍🏿👍🏿👍🏿 좋은 걸 날름 많이 알게 됐습니다 ㅋㅋ

1개의 답글
comment-user-thumbnail
2022년 10월 28일

와!! 좋은 프로젝트라고 생각합니다. 요새는 정보가 너무 많아져서 오히려 잘 선별된 컨텐츠를 찾는 것이 너무 피곤했는데 양질의 자료들을 또 볼 수 있는 곳이 새롭게 생긴것 같아서 좋네요!

1개의 답글
comment-user-thumbnail
2022년 10월 29일

라이브보면서 많이 배웠습니다~ 다음에 또 진행하시면 채팅도 많이 해야겠어요 😆
그리고 Chaf 응원도 많이 하겠습니다~~

답글 달기
comment-user-thumbnail
2022년 12월 2일

비슷한 주제의 사이드 프로젝트를 구상하고 있었는데, 정말 많은 인사이트를 알고 갑니다.

멋지십니다. 좋은 글 감사합니다 👍🏻

답글 달기
comment-user-thumbnail
2023년 1월 17일

안녕하세요 !

혹시 리믹스를 사용하심에도 불구하고 백엔드/프론트엔드를 분리하신 이유가 있으실까요 ?

단순 리백트 개발 튜토리얼을 대체하기 위한 프로젝트이기 때문에 분리하신걸까요 ?

답글 달기