NestJS + Next.js 모노레포(Monorepo)에서 글로벌 브랜딩 모듈 구현하기
요약
NestJS와 Next.js 모노레포 환경에서 글로벌 브랜딩 설정을 관리하기 위한 모듈 구현 방법을 다룹니다. 데이터베이스 마이그레이션부터 API 컨트롤러 구축, UI 연결까지 풀스택 관점의 동기화 과정을 설명합니다.
핵심 포인트
- 모노레포 내 API와 UI 간의 데이터 동기화 전략
- Zod를 활용한 런타임 유효성 검사 및 안정적인 API 설계
- 기존 배포를 깨뜨리지 않는 단계적 데이터베이스 마이그레이션 방법
- 브랜딩 설정을 위한 단일 진실 공급원(SSOT) 구축
NestJS + Next.js 모노레포(Monorepo)에서 글로벌 브랜딩 모듈 구현하기
요약 (TL;DR):
타이포그래피(typography), 배경(background), 배너(banner), 푸터(footer) 옵션을 노출하기 위해 branding_settings 테이블과 전용 NestJS 컨트롤러(controller)를 추가한 후, 해당 설정을 Next.js 관리자 UI(admin UI)에 연결했습니다. 이를 통해 CRM에 브랜딩을 위한 단일 진실 공급원(single source of truth)을 제공하며, 모노레포(monorepo) 전반에서 UI와 API를 동기화하는 방법을 보여줍니다.
문제 상황
기존 CRM에는 몇 가지 하드코딩된 브랜드 자산(logo, colors)이 있었습니다. 제품 팀에서 사용자가 타이포그래피(typography), 배경 이미지(background images), 배너(banners), 푸터(footers)를 조정할 수 있도록 하고 싶었을 때, 데이터베이스 스키마(database schema)에는 필요한 컬럼(column)이 누락되어 있었고, API에는 이러한 값들을 업데이트할 깔끔한 엔드포인트(endpoint)가 부족했습니다. 그 증상은 /api/branding에 대한 404 오류와 DB에 존재하지 않는 설정을 PATCH 하려고 할 때 발생하는 500 오류였습니다.
처음 시도했던 것들
- 기존
branding.controller.ts패치 – PATCH 라우트(route)를 추가했지만, 컨트롤러(controller)에zod스키마(schema)와db.query헬퍼(helper)가 누락되어 컴파일 타임(compile time)에 실패했습니다. - UI 페이지에 직접 필드 추가 –
page.tsx에 폼 컨트롤(form controls)을 삽입했지만, 백엔드(backend) 라우트가 등록되지 않아 POST 요청이 404 오류를 발생시켰습니다. - 마이그레이션(migration) 즉시 수정 –
20260728_branding_settings.sql에 새 컬럼을 추가했지만, 마이그레이션 순서가 CI 파이프라인(pipeline)을 깨뜨렸습니다. 테이블이 이미 이전 스키마로 생성되어 있었기 때문입니다.
각 시도는 API 레이어(layer), 데이터베이스 스키마(database schema), 그리고 UI 사이의 더 깊은 결합(coupling)을 드러냈으며, 점진적인 수정 작업을 취약하게 만들었습니다.
구현 내용
1. 데이터베이스 마이그레이션 (Database Migration) – 20260728_branding_settings.sql & 20260729_branding_settings_extended.sql
-- 20260728_branding_settings.sql
CREATE TABLE branding_settings (
id SERIAL PRIMARY KEY,
...
왜 두 번의 마이그레이션을 진행했나요?
첫 번째 마이그레이션은 이전 기능에서 사용하던 기본 테이블을 생성했습니다. 두 번째 마이그레이션은 기존 배포를 깨뜨리지 않으면서 새로운 UI 컨트롤을 담을 수 있도록 테이블을 확장했습니다.
2. API Layer – apps/api/src/branding/branding.controller.ts
import { Controller, Get, Patch, Body, UseGuards } from '@nestjs/common';
import { z } from 'zod';
import { query as db } from '../db/db.js';
...
핵심 포인트 (Key points):
- 단일 행 (Single row) (
id = 1)을 사용하여 모듈을 다른 데이터로부터 격리합니다. - Zod를 통해 런타임 유효성 검사 (Runtime validation) 및 명확한 에러 메시지를 제공합니다.
- AuthGuard +
RequirePerm를 사용하여 권한이 있는 사용자만이 브랜딩을 변경할 수 있도록 강제합니다.
3. 컨트롤러 등록 – apps/api/src/app.module.ts
import { BrandingController } from './branding/branding.controller.js';
@Module({
...
여기에 컨트롤러를 추가하면 API에서 해당 라우트(Route)를 사용할 수 있게 됩니다.
4. 프론트엔드 – apps/web/src/app/settings/branding/page.tsx
"use client";
import { useEffect, useState } from "react";
import { AuthGuard } from "../../_components/AuthGuard";
...
주요 특징 (Highlights):
- React hooks를 사용하여 데이터를 가져오고 변경(Mutate)합니다.
- 일관된 레이아웃과 권한 적용을 위해 페이지를
AuthGuard및CrmShell로 감쌉니다. - 폼(Form)은 의도적으로 단순하게 구성되었습니다. 유효성 검사는 Zod를 통해 서버 측에서 처리됩니다.
5. UI Shell 업데이트 – CrmShell.tsx & CrmShellCondos.tsx
두 컴포넌트 모두 이제 /settings/branding 라우트를 포함하는 NAV_BRANDING_GROUP 상수를 임포트합니다. 이를 통해 branding:manage 권한을 가진 사용자에게 사이드바에 새 페이지가 나타나도록 보장합니다.
const NAV_BRANDING_GROUP: NavGroup = {
group: "Branding",
icon: "🖌️",
...
핵심 요약 (Key Takeaway)
설정(Configuration)을 타입이 잘 지정된 단일 테이블에 유지하고, Zod로 입력을 검증하는 전용 컨트롤러를 통해 노출하십시오.
이 패턴은 브랜딩을 다른 모듈로부터 격리하고, 결합도(Coupling)를 낮추며, UI가 사용할 수 있는 깔끔한 API 표면(API surface)을 제공합니다. 또한 확장성도 뛰어납니다. 나중에 더 많은 브랜딩 필드를 추가하는 것은 단순히 마이그레이션(Migration)과 몇 개의 폼 필드를 추가하는 작업일 뿐입니다.
다음 단계 (What's Next)
제 Build in Public 시리즈의 일부입니다 — 멕시코 플라야 델 카르멘(Playa del Carmen)에서 Building PlayaMXCRM을 구축하는 실제 과정을 공유합니다.
Repo: zaerohell/VS · 2026-07-29
#playadev #buildinpublic
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기