Payload CMS 완벽 가이드 | Headless CMS·TypeScript·Admin Panel·실전 활용

Payload CMS 완벽 가이드 | Headless CMS·TypeScript·Admin Panel·실전 활용

이 글의 핵심

Payload CMS로 강력한 콘텐츠 관리를 구현하는 완벽 가이드입니다. TypeScript 네이티브, Admin Panel, Access Control, Hooks까지 실전 예제로 정리했습니다.

실무 경험 공유: Strapi에서 Payload로 전환하면서, 타입 안전성이 향상되고 커스터마이징이 자유로워진 경험을 공유합니다.

들어가며: “CMS 커스터마이징이 어려워요”

실무 문제 시나리오

시나리오 1: 타입 안전성이 부족해요
기존 CMS는 타입이 약합니다. Payload는 완벽한 TypeScript 지원을 제공합니다.

시나리오 2: Admin UI가 고정돼 있어요
커스터마이징이 제한적입니다. Payload는 React 기반으로 자유롭습니다.

시나리오 3: 복잡한 로직이 필요해요
제한적입니다. Payload는 Hooks로 모든 것을 제어할 수 있습니다.


1. Payload CMS란?

핵심 특징

Payload는 TypeScript 기반 Headless CMS입니다.

주요 장점:

  • TypeScript: 완벽한 타입 안전성
  • Admin Panel: React 기반 UI
  • Access Control: 세밀한 권한 관리
  • Hooks: 라이프사이클 제어
  • Local API: 서버리스 친화적

2. 프로젝트 설정

설치

npx create-payload-app@latest

프로젝트 구조

my-payload-app/
├── src/
│   ├── collections/
│   │   ├── Users.ts
│   │   └── Posts.ts
│   ├── payload.config.ts
│   └── server.ts
└── package.json

3. Collection 정의

Posts Collection

// src/collections/Posts.ts
import { CollectionConfig } from 'payload/types';

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
  },
  access: {
    read: () => true,
  },
  fields: [
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'content',
      type: 'richText',
      required: true,
    },
    {
      name: 'author',
      type: 'relationship',
      relationTo: 'users',
      required: true,
    },
    {
      name: 'status',
      type: 'select',
      options: [
        { label: 'Draft', value: 'draft' },
        { label: 'Published', value: 'published' },
      ],
      defaultValue: 'draft',
    },
    {
      name: 'publishedAt',
      type: 'date',
    },
    {
      name: 'featuredImage',
      type: 'upload',
      relationTo: 'media',
    },
  ],
};

4. Access Control

기본 권한

export const Posts: CollectionConfig = {
  slug: 'posts',
  access: {
    read: () => true,
    create: ({ req: { user } }) => !!user,
    update: ({ req: { user } }) => !!user,
    delete: ({ req: { user } }) => user?.role === 'admin',
  },
  fields: [
    // ...
  ],
};

Field Level Access

{
  name: 'internalNotes',
  type: 'textarea',
  access: {
    read: ({ req: { user } }) => user?.role === 'admin',
    update: ({ req: { user } }) => user?.role === 'admin',
  },
}

5. Hooks

beforeChange

export const Posts: CollectionConfig = {
  slug: 'posts',
  hooks: {
    beforeChange: [
      ({ data, operation }) => {
        if (operation === 'create') {
          data.createdAt = new Date().toISOString();
        }
        data.updatedAt = new Date().toISOString();
        return data;
      },
    ],
  },
  fields: [
    // ...
  ],
};

afterChange

hooks: {
  afterChange: [
    async ({ doc, operation }) => {
      if (operation === 'create') {
        await sendNotification(`New post: ${doc.title}`);
      }
    },
  ],
}

6. REST API

자동 생성

# 모든 포스트
GET /api/posts

# 단일 포스트
GET /api/posts/:id

# 생성
POST /api/posts

# 업데이트
PATCH /api/posts/:id

# 삭제
DELETE /api/posts/:id

사용

// 조회
const response = await fetch('http://localhost:3000/api/posts');
const { docs } = await response.json();

// 생성
const response = await fetch('http://localhost:3000/api/posts', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'New Post',
    content: 'Content here',
    author: userId,
  }),
});

7. Local API

import payload from 'payload';

// 조회
const posts = await payload.find({
  collection: 'posts',
  where: {
    status: {
      equals: 'published',
    },
  },
  limit: 10,
});

// 생성
const post = await payload.create({
  collection: 'posts',
  data: {
    title: 'New Post',
    content: 'Content',
    author: userId,
  },
});

// 업데이트
await payload.update({
  collection: 'posts',
  id: postId,
  data: {
    title: 'Updated Title',
  },
});

8. Next.js 통합

설정

// payload.config.ts
import { buildConfig } from 'payload/config';

export default buildConfig({
  serverURL: 'http://localhost:3000',
  collections: [Posts, Users, Media],
  admin: {
    user: 'users',
  },
  typescript: {
    outputFile: './payload-types.ts',
  },
});

사용

// app/blog/page.tsx
import payload from 'payload';

async function getPosts() {
  const { docs } = await payload.find({
    collection: 'posts',
    where: {
      status: { equals: 'published' },
    },
  });

  return docs;
}

export default async function BlogPage() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  );
}

정리 및 체크리스트

핵심 요약

  • Payload CMS: TypeScript Headless CMS
  • Admin Panel: React 기반 UI
  • Access Control: 세밀한 권한
  • Hooks: 라이프사이클 제어
  • Local API: 서버리스 친화적
  • REST API: 자동 생성

구현 체크리스트

  • Payload 설치
  • Collection 정의
  • Access Control 설정
  • Hooks 구현
  • REST API 사용
  • Next.js 통합
  • 배포

같이 보면 좋은 글

  • Sanity CMS 완벽 가이드
  • Next.js App Router 가이드
  • Strapi 가이드

이 글에서 다루는 키워드

Payload CMS, Headless CMS, TypeScript, Admin Panel, Content, Backend, Node.js

자주 묻는 질문 (FAQ)

Q. Strapi와 비교하면 어떤가요?

A. Payload가 TypeScript 지원이 더 좋고 커스터마이징이 자유롭습니다.

Q. Sanity와 비교하면 어떤가요?

A. Payload는 셀프 호스팅이 쉽고 Admin UI가 내장되어 있습니다.

Q. 무료인가요?

A. 네, 오픈소스이고 무료입니다.

Q. 프로덕션에서 사용해도 되나요?

A. 네, 많은 기업에서 안정적으로 사용하고 있습니다.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3