
블로그 서버를 Go 기반의 Echo 프레임워크에서 TypeScript 기반의 NestJS로 옮기기로 했다. 이유는 다음과 같다.
1. 프론트엔드와 개발 언어 통일
기존에는 백엔드는 Go로, 프론트엔드는 TypeScript로 개발하고 있었다. 초기에 백엔드 언어로 Go를 선택한 것은 Go 언어를 공부하면서 실제로 활용해보기 위함이었으나 아쉽게도 크게 도움이 되진 못했다. 언어와 프레임워크 모두 미숙한 상태로 개발을 진행하다 보니, 기능을 수정하거나 추가하는데 있어 시간이 오래 소요되었다.
2. 모노레포 구조를 통해 개발 편의성 향상
모노레포 구조를 통해 프론트엔드와 백엔드를 하나의 리포지터리에 두면서 개발 편의성을 향상시킬 목적이었다. 기존에는 프론트엔드(TS)-백엔드(Go)이다보니 공통으로 사용할 수 있는 패키지 같은 것이 없었다. 하지만 프론트-백 언어를 통일한다면 양쪽에서 사용하는 공통적인 패키지(예: supabase)나 코드를 수월하게 관리할 수 있다.
3. 프로젝트 경로 일원화를 통한 AI의 컨텍스트 이해도 증가
GeminiCLI를 개발에 요긴하게 사용하고 있는데, 프로젝트가 두 개로 나뉘어져 있을 때는 GeminiCLI에게 필요한 정보를 전달해주기 번거로웠다. GeminiCLI는 자신이 실행된 경로 외의 파일들은 읽지 못하기에 기능 개발을 할 때 데이터 모델을 멋대로 구성하는 등의 문제를 일으켰다. GEMINI.MD를 통해 사전정보를 제공해도 무시하는 경우가 종종 일어나서 비효율적이라고 판단했다.
Go가 가진 성능 상의 이점들(TS 대비 빠른 속도, goroutine을 이용한 동시성 프로그래밍) 때문에 처음에 백엔드에 Go를 사용한 것이었지만, 블로그 기능이 어느 정도 갖추어진 지금 와서 보니 Go의 고성능을 활용할만한 부분은 없었다.
추후 고성능이 필요한 기능이 있다면 그 때 가서 MSA 형태로 Go 서버를 다시 구성하기로 했다. 나같은 주니어에게는 다양한 언어보단 한두 가지를 확실히 익히는 게 낫다 생각했다.
pnpm과 Turborepo는 모노레포를 구성할 때 가장 많이 사용되는 방식이다. pnpm은 npm과 비교하여 저장공간이나 속도 면에서 효율적이고, Turborepo는 고성능 빌드 시스템으로, 한 번 작업을 수행하며 수행한 계산은 캐시에 저장하여 이후 실행에서는 생략하여 빌드 속도를 높인다.
그럼 우선 두 개로 나뉘어져있던 프로젝트를 하나로 통합해야할 것이다.
프로젝트 소스는 프론트엔드 git 소스를 기반으로 통일하기로 했다.
우선 apps 폴더를 만들고 그 하위에 web 과 server 폴더를 만들었다. 그리고 프론트엔드 소스를 모두 web 쪽으로 옮겼다. 루트에는 pnpm-workspace.yaml 파일을 만들었다. pnpm-workspace.yaml은 워크스페이스의 루트를 정의하고 워크스페이스에 디렉터리를 포함/제외 하는 역할을 한다고 한다. 우선은 생성된 파일에
# pnpm-workspace.yaml
packages:
- "apps/*"
내용을 채워넣었다. 추후 공통으로 사용할 코드가 생길 시 packages 폴더를 만들고 pnpm-workspace.yaml에 추가할 예정이다.
다음으로 루트 경로에서 > pnpm init 으로 package.json을 생성했다.
생성된 package.json은 다음과 같았다.
{
"name": "[내 프로젝트명]",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"packageManager": "pnpm@10.20.0"
}
packageManager에 pnpm@10.20.0 버전을 사용한다고 명시되어 있다.
여기까지 하면 프로젝트의 구조는 다음과 같이 된다.
.
├─ apps/
│ ├─ web/ # Next.js
│ └─ server/ # NestJS
├─ turbo.json
├─ pnpm-workspace.yaml
└─ package.json
시험 삼아 pnpm install을 실행 후 pnpm --filter [패키지명] dev 를 하니 정상적으로 사이트가 나온다. 일단 여기까지는 완료.
문제는 백엔드였다. 프론트 쪽은 패키지매니저만 바뀌는 수준이라면, 백엔드는 개발 언어부터 싹 바뀌는 구조였다. 쉽게 성공하지는 않을 거라는 말. 일단 server 쪽에 NestJS 프로젝트를 만드는 것부터 시작했다. NestJS의 경우 CLI를 통해 프로젝트 구조를 쉽게 구성할 수 있어서 편리했다.
일단 기본적인 백엔드 서버만 만들어놓고 Turborepo를 도입하기로 했다. 기존에 있는 프로젝트에 Turborepo를 도입하는 것이기에 Turborepo 공식 문서의 해당 목차를 참고하여 프로젝트를 구성했다. Turborepo 공식 매뉴얼에서는 turbo를 global과 프로젝트에 각각 설치하기를 권장하고 있는데, global은 CLI 편의용이고 실제 빌드/배포/CI는 로컬 설치본이 사용되기 때문에 global 설치는 필수는 아니다.
turbo 설치 후 루트에 turbo.json 파일을 만든다. 내용은 공식 문서의 매뉴얼을 그대로 따랐다.
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"]
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"persistent": true,
"cache": false
}
}
}
그 후 turbo dev를 실행했다. 역시 오류가 쫘악 떨어진다. 일단 하나하나 살펴봤다. tasks는 각 패키지의 동일 task에 대한 캐시/의존/출력/env 설정 맵이다. turbo 뒤에 입력하는 명령어 리스트…라고 보면 될까. turbo build, turbo check-types, turbo dev 등의 명령어를 통해 web과 server 경로에 있는 프로젝트의 task를 실행하는 것이라고 이해했다. 그렇다면 build가 웹과 서버를 모두 빌드하는 것이 맞긴한데… outputs가 next 쪽으로만 되어있어서 수정이 필요할 것 같았다. 음 그럼 어떻게?
지금 모노레포에는 두가지 프로젝트가 있다. web과 server. 그리고 웹과 서버가 요구하는 env 값이나 output 경로 같은도 다를 것이다. 그러면 어떻게 하느냐? 해결 방법은 이러했다. 각 프로젝트의 build를 나누면 된다.
{
"$schema": "https://turborepo.com/schema.json",
"tasks": {
"web#build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "!.next/cache/**"],
"env": ["프론트에 필요한", "ENV", "키"] // 이 값이 변경되면 web build의 캐시 무효화
},
"server#build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"],
"env": ["서버에 필요한", "ENV", "키"] // 이 값이 변경되면 server build의 캐시 무효화
},
"check-types": {
"dependsOn": ["^check-types"]
},
"dev": {
"persistent": true,
"cache": false
}
}
}
이렇게 하면 turbo build를 했을 때 web의 build와 server의 build를 수행하면서 알맞게 필요한 설정들을 넣어준다(env 같은 경우 Turborepo가 프레임워크 추론을 통해 각 프레임워크에 기본적으로 붙은 prefix는 자동으로 포함한다. 따라서 그런 prefix가 붙지 않는 env 값만 turbo.json에 추가해주면 된다). 이렇게 하니 build가 정상적으로 실행되는 것을 확인할 수 있었다. turbo build를 다시 입력하면 이런 화면을 볼 수 있는데, nextjs 프로젝트는 기존 캐싱한 빌드를 바로 보여주고, nestjs 프로젝트만 다시 빌드한 것을 알 수 있다(nestjs 빌드를 캐싱하는 방법도 있을 것 같은데 이는 좀 더 나중에 찾아보려고 한다).
개발 할 때는 어떨까? 기존에는 Go 프로젝트 가서 go run main.go, Next.js 프로젝트 가서 npm run dev 를 했었다. 은근히 귀찮았던 작업이다. 하지만 이제는 turbo dev 만 실행하면…
안 된다. 왜 그런 것인가 다시 봤는데 의외로 간단했다. turbo.json에 있는 tasks는 기본적으로 각 프로젝트의 명령어를 실행시키는 것이다. turbo build를 하면 각 프로젝트의 build 명령어가 수행되는 방식이다. Next.js 프로젝트에는 dev 명령어가 있는데 NestJS 프로젝트에는 없었다. 그게 전부였다. 그래서 NestJS 프로젝트의 package.json에 들어가서 "dev": "nest start --watch"를 추가해줬다.
이제야 turbo dev를 실행하니 프론트와 백엔드 프로젝트가 동시에 실행되는 것을 확인할 수 있었다. 많이 까다로운 작업일 거라 생각했지만, 하나 하나 파고들어가니 무리 없이 해낼 수 있었다.