크롬 익스텐션 하나였던 프로젝트를 Turborepo 모노레포로 옮긴 이야기

크롬 익스텐션 하나였던 프로젝트를 Turborepo 모노레포로 옮긴 이야기

2026년 3월 6일

크롬 익스텐션 하나로 시작한 프로젝트에 랜딩 페이지와 면접 결과 공유 뷰어가 필요해졌다.

세 앱이 같은 Supabase 클라이언트와 타입을 공유해야 했기 때문에 모노레포 전환을 결정했다.

“폴더 하나 만들고 파일 옮기면 되겠지”라고 생각했지만, WXT의 빌드 방식이 일반 Vite 앱과 달라서 파이프라인 설정을 따로 다뤄야 했다.


1. 문제 상황

면접털림은 처음에 크롬 익스텐션 하나였다.

프로그래머스 문제 페이지에서 AI 면접관과 대화하고, 알고리즘 문제를 간격 반복으로 복습하는 도구다.

그러다 두 가지가 추가로 필요해졌다.

  • 랜딩 페이지: 크롬 웹스토어 링크와 기능 소개
  • 공유 뷰어: 면접 결과 리포트를 URL로 공유하면 브라우저에서 볼 수 있는 페이지

세 앱 모두 Supabase를 쓰고, 리포트 타입을 공유한다. 별도 레포로 관리하면 타입을 복사-붙여넣기 해야 하거나 npm 패키지로 올려야 한다.

자연스럽게 모노레포를 택했다.

레퍼런스로 이전 프로젝트인 hanspoon-frontend를 참고했다.

2. 내가 처음 한 생각

구조는 단순하게 잡았다.

apps/
extension/ # 기존 WXT
익스텐션
landing/ # Vite + React
viewer/ # Vite + React
packages/
shared/ # 공통 타입
(추후)

Turborepo는 build 태스크만 설정하고, dev는 각 앱에서 따로 실행하면 되겠다고 봤다.

pnpm workspace 설정도 간단할 거라 생각했다.

# pnpm-workspace.yaml
packages:
  - "apps/*"
  - "packages/*"

파일을 apps/extension/으로 옮기고, 루트 package.json을 새로 쓰면 끝날 것 같았다.

3. 실제로 해 본 선택

파일 이동은 그대로였다

src/, entrypoints/, public/, wxt.config.ts를 apps/extension/ 안으로 옮겼다. apps/extension/package.json에 name만 변경했다.

{
  "name": "@interview-teolrim/extension"
}

turbo.json은 WXT 출력 경로를 명시해야 했다.

WXT는 빌드 결과를 .output/에 쓴다. Turborepo의 캐시가 이 경로를 알아야 하므로 명시했다.

{
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".output/**", "dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

dev는 cache: false와 persistent: true가 필요하다. WXT와 Vite 모두 watch 모드로 계속 떠있기 때문이다.

postinstall이 루트에서 실행되면 안 됐다

기존 package.json에 있던 “postinstall”: “wxt prepare”가 루트로 올라오면서 문제가 생겼다. 루트 디렉토리에서 wxt prepare를 실행하면 wxt.config.ts를 찾지 못한다.

해결 방법은 간단했다. postinstall은 apps/extension/package.json에만 두고,루트 package.json에서는 제거했다.

루트 package.json에서 삭제

"postinstall": "wxt prepare" ← 제거

apps/extension/package.json에만 유지

"postinstall": "wxt prepare"
← 유지

tsconfig.json이 두 개로 분리됐다

WXT는 .wxt/tsconfig.json을 자동 생성하고, apps/extension/tsconfig.json이 이걸 extends한다. 이 구조는 그대로 유지했다.

landing과 viewer는 별도 tsconfig.json을 각각 가진다. 루트에 공통 tsconfig를 두려했지만, WXT의 generated 타입 경로 때문에 섞으면 충돌이 생겼다. 세 앱이 각자 tsconfig를 갖는 게 더 단순했다.

biome.json은 루트 하나로 통합했다

포매터와 린터 설정은 루트에 하나만 두고, files.ignore로 빌드 산출물을 제외했다.

{
"files": {
  "ignore": [
    "**/node_modules/**",
    "**/.output/**",
    "**/dist/**",
    "**/.wxt/**"
    ]
  }
}

landing과 viewer는 Vite 그대로

pnpm create vite apps/landing —template react-ts pnpm create vite apps/viewer —template react-ts

TailwindCSS 4는 @tailwindcss/vite 플러그인으로 추가했다. extension과 동일한 방식이라 별다른 설정이 없었다.

// vite.config.ts (landing,viewer 공통)
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});

4. 결과

전환 후 루트에서 pnpm install을 실행하면 세 앱의 의존성이 한 번에 설치된다.

pnpm —filter @interview-teolrim/extension dev pnpm —filter @interview-teolrim/landing dev pnpm —filter @interview-teolrim/viewer dev

전체 빌드는 루트에서 한 번에

turbo build

Turborepo가 변경된 앱만 다시 빌드하고 나머지는 캐시에서 복원한다.

Supabase 클라이언트는 extension과 viewer가 각자 src/lib/supabase.ts를 갖는다. 지금은 파일이 두 개지만, 내용이 동일하다. 나중에 packages/shared로 올릴 예정이다.

실패한 것도 있었다.

turbo dev로 세 앱을 동시에 띄우면 WXT의 dev 서버 로그와 Vite의 로그가 섞여서 읽기 어려웠다. 지금은 앱별로 터미널을 따로 열어서 실행한다.

5. 지금 다시 해본다면?

처음부터 postinstall 스크립트 위치를 명확하게 잡을 것이다.

WXT 특유의 스크립트(wxt prepare)가 루트에서 실행되면 안 된다는 걸 나중에 알았다. 모노레포에서 WXT를 쓴다면, apps/extension/package.json에만 postinstall을 두는 게 맞다.

tsconfig 통합도 시도했다가 되돌렸다. WXT가 generated 타입을 .wxt/에 쌓는 방식 때문에 루트 tsconfig와 섞으면 경로 충돌이 생긴다. 앱별로 tsconfig를 분리하는 게 WXT와 공존하는 가장 마찰이 적은 방법이다.

그리고 packages/shared는 처음부터 만들 필요가 없다. 두 앱 이상에서 실제로 같은 코드를 쓸 때 만들면 충분하다. 지금은 Supabase 클라이언트 생성 코드가 두 곳에 같은 내용으로 있는데, 이게 불편해질 때 올릴 생각이다.