무색
기술블로그
에세이
연구
프로덕트
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

[email protected]

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관
INCHEON, KR
PayloadCMS + Next.js 15로 회사 홈페이지 만들기
museck 만들기
2026. 1. 6.

PayloadCMS + Next.js 15로 회사 홈페이지 만들기

PayloadCMSNext.jsReactTailwind CSSHeadless CMS

회사 홈페이지를 새로 만들어야 했다. 기존에 쓰던 템플릿 사이트는 수정할 때마다 개발자 손을 거쳐야 했고, 콘텐츠 하나 바꾸는 데 PR을 올려야 하는 상황이 반복됐다. 그래서 이번엔 비개발자도 콘텐츠를 직접 관리할 수 있으면서, 개발자는 코드로 구조를 통제할 수 있는 아키텍처를 찾았다.

결론부터 말하면 PayloadCMS 3.x + Next.js 15 App Router 조합을 골랐다. Headless CMS가 콘텐츠 관리를 담당하고, Next.js가 프론트엔드를 렌더링하는 구조다. 이 글에서는 프로젝트 초기화부터 디자인 시스템 설정, 블록 기반 페이지 빌더 구현까지의 과정을 정리한다.

왜 PayloadCMS + Next.js인가

Headless CMS 후보는 여럿 있었다. Strapi, Sanity, Contentful 등을 비교했는데, PayloadCMS를 선택한 건 몇 가지 이유가 있다.

  • 셀프 호스팅이 가능하다. 데이터가 내 서버에 있으니 종속성 걱정이 없다. MongoDB만 띄우면 된다.
  • Next.js와 같은 프로세스에서 돌아간다. PayloadCMS 3.x부터는 Next.js 플러그인 형태로 통합된다. 별도 서버를 띄울 필요가 없어졌다.
  • TypeScript 네이티브다. 컬렉션 정의부터 API 응답까지 전부 타입이 잡힌다. pnpm generate:types 한 번이면 Payload 타입을 자동 생성해준다.
  • 관리자 패널이 기본 내장이다. 따로 어드민 UI를 만들 필요 없이 https://도메인/admin으로 접근하면 된다.

Next.js 15를 선택한 건 App Router가 안정화됐고, React 19의 Server Components를 제대로 활용할 수 있어서다. Tailwind CSS는 디자인 토큰을 코드로 관리하기에 가장 편했다.

Tailwind 디자인 시스템 구축

프로젝트 초반에 디자인 토큰부터 잡았다. 색상, 폰트, 타이포그래피 스케일, 그림자, 라운딩 값을 tailwind.config.ts에 한곳에 모아두면 나중에 디자인 변경이 훨씬 수월해진다.

// tailwind.config.ts
colors: {
  background: { primary: "#FAFAFA", secondary: "#F5F5F5" },
  foreground: { primary: "#1A1A1A", secondary: "#6B6B6B" },
  accent: { DEFAULT: "#4A6CF7", hover: "#3B5AE0" },
},
fontFamily: {
  sans: ["Pretendard", "-apple-system", "sans-serif"],
},
fontSize: {
  hero: ["3rem", { lineHeight: "1.2" }],
  h1: ["2.25rem", { lineHeight: "1.3" }],
  h2: ["1.75rem", { lineHeight: "1.4" }],
  h3: ["1.25rem", { lineHeight: "1.5" }],
},

포인트가 몇 가지 있다. background과 foreground를 의미 기반(semantic)으로 나눠서 다크 모드 대응을 쉽게 했다. Pretendard 폰트는 한글 가독성이 좋고 웹폰트 지원이 안정적이다. 타이포그래피 스케일은 hero부터 h3까지 네 단계로 잡아서 페이지 위계를 명확하게 표현한다.

컬렉션과 블록 설계

PayloadCMS에서 콘텐츠 구조는 컬렉션(Collection)으로 정의한다. 이번 프로젝트에서는 5개 컬렉션을 만들었다.

  • Users - 관리자 계정
  • Media - 이미지/파일 업로드
  • Posts - 블로그 글
  • Projects - 포트폴리오
  • Pages - 동적 페이지 (홈, 소개 등)

Pages 컬렉션이 핵심인데, 여기에 블록 기반 페이지 빌더 패턴을 적용했다. PayloadCMS의 blocks 필드 타입을 활용하면 관리자가 레이아웃 블록을 레고처럼 조합해서 페이지를 만들 수 있다.

정의한 블록은 5개다.

  • Hero - 페이지 상단 히어로 섹션
  • Content - 리치 텍스트 본문
  • Features - 기능/특징 그리드
  • CTA - 콜 투 액션 버튼
  • ImageText - 이미지 + 텍스트 2단 레이아웃

WordPress의 Gutenberg나 Strapi의 Dynamic Zone과 비슷한 개념이다. 차이점은 블록 정의가 코드(TypeScript)로 되어 있어서 타입 안전성을 보장받는다는 점이다.

RenderBlocks 패턴

프론트엔드에서는 blockType 값을 기준으로 블록 컴포넌트를 동적으로 렌더링한다. switch-case로 단순하게 구현했다.

export function RenderBlocks({ blocks }: { blocks: LayoutBlock[] }) {
  return blocks.map((block, i) => {
    switch (block.blockType) {
      case "hero":
        return <HeroBlockComponent key={i} block={block} />
      case "features":
        return <FeaturesBlockComponent key={i} block={block} />
      case "content":
        return <ContentBlockComponent key={i} block={block} />
      case "cta":
        return <CTABlockComponent key={i} block={block} />
      case "imageText":
        return <ImageTextBlockComponent key={i} block={block} />
      default:
        return null
    }
  })
}

이 패턴이 좋은 건 새 블록을 추가할 때 영향 범위가 명확하다는 거다. 블록 정의 파일 하나, 컴포넌트 하나, switch case 한 줄만 추가하면 된다. 기존 블록에는 전혀 영향을 주지 않는다.

Slug 자동 생성

URL 경로에 쓸 slug를 매번 수동으로 입력하는 건 번거롭다. PayloadCMS의 beforeValidate 훅에서 title 값을 kebab-case로 자동 변환하도록 만들었다.

// fields/slug.ts
hooks: {
  beforeValidate: [({ value, data }) => {
    if (!value && data?.[fieldToUse]) {
      return data[fieldToUse]
        .toLowerCase()
        .replace(/[^a-z0-9가-힣]+/g, "-")
        .replace(/^-|-$/g, "")
    }
    return value
  }],
}

정규식에서 가-힣 범위가 한글 음절이다. 영문과 숫자, 한글만 남기고 나머지는 하이픈으로 치환한다. 관리자가 slug를 직접 입력하면 그 값을 그대로 사용하고, 비어 있을 때만 title에서 자동 생성한다.

Lexical richText 시리얼라이저

PayloadCMS는 리치 텍스트 에디터로 Lexical을 쓴다. DB에는 Lexical JSON 형식으로 저장되는데, 프론트엔드에서 이걸 React 컴포넌트로 변환해야 한다. 노드 타입별로 매핑하는 시리얼라이저를 직접 구현했다.

heading, paragraph, list, code, link, quote, upload 등의 노드를 각각 대응하는 React 컴포넌트로 렌더링한다. 코드 블록은 code-highlight 노드를 줄 단위로 처리하고, 이미지는 upload 노드의 Media ID를 참조해서 보여준다.

돌아보며

이 초기 설정 단계에서 크게 삽질한 건 없었다. 하지만 나중에 Docker 컨테이너화와 CI/CD를 붙이면서 ESM 호환성 문제가 줄줄이 터졌다. "type": "module" 프로젝트에서 CJS 설정 파일(postcss.config.cjs 같은)을 쓸 때 확장자를 명시적으로 .cjs로 해야 하는 게 대표적이다.

그래서 하나 배운 게 있다면, 초기 구조를 잡을 때 빌드/배포 환경까지 미리 고려해야 한다는 거다. 로컬에서 잘 돌아간다고 끝이 아니다. Docker에서 빌드할 때 DB가 없는 상태에서도 빌드가 통과해야 하고, ESM/CJS 호환성도 처음부터 확인해야 한다.

프로젝트 초기에 디자인 토큰과 컴포넌트 구조를 잘 잡으면 이후 확장이 훨씬 수월하다. Headless CMS + 블록 기반 페이지 빌더는 개발자와 콘텐츠 관리자 모두를 만족시키는 아키텍처다.

다음 글에서는 이 프로젝트를 Docker로 컨테이너화하고 Kubernetes에 배포하는 과정을 다룰 예정이다.

자주 묻는 질문

PayloadCMS 3.x와 Next.js 15를 함께 쓰면 어떤 장점이 있나요?
PayloadCMS가 Next.js App Router에 내장되어 별도 백엔드 서버 없이 하나의 프로세스로 CMS와 프론트엔드를 운영합니다. 비개발자는 admin UI에서 콘텐츠를 관리하고 개발자는 코드로 구조를 통제할 수 있습니다.
PayloadCMS의 블록 기반 페이지 빌더는 어떻게 동작하나요?
Hero, Grid, Text 같은 블록 타입을 정의해두고 에디터가 블록을 조합해 페이지를 만듭니다. 개발자는 블록별 React 컴포넌트를 구현하고 비개발자는 admin에서 블록을 배치해 페이지를 구성합니다.
PayloadCMS에서 디자인 토큰을 관리하는 방법은?
Tailwind config에 색상, 타이포, 간격 등 디자인 토큰을 정의하고 CSS 변수로 내보냅니다. 컴포넌트에서 토큰 값을 참조하면 디자인 시스템이 코드 전체에 일관되게 적용됩니다.
museck 만들기(1/10)
Next

Next.js 15 standalone 앱을 Docker와 K8s로 배포하기