Communitygithub.com

Strapi 패턴

오픈소스 헤드리스 CMS — 콘텐츠 타입 스키마, REST API, Document Service API, 커스텀 컨트롤러, 라이프사이클 훅, 인증, TypeScript

Strapi 패턴란 무엇인가요?

이 스킬은 Strapi를 오픈소스 헤드리스 CMS로 사용하는 방법을 상세히 안내합니다. v5에서는 Entity Service API가 제거되었으므로, 백엔드 데이터 접근 시 반드시 Document Service API(`strapi.documents()`)를 사용해야 하며, REST API 응답은 `data.fieldName`과 같은 평평한 형식을 따릅니다. 관계형 데이터와 미디어는 기본적으로 채워지지 않으므로 `populate` 매개변수를 항상 명시해야 합니다. 복잡한 쿼리 문자열을 작성할 때는 `qs` 라이브러리를 사용하는 것이 권장됩니다. 모든 콘텐츠 타입 엔드포인트는 기본적으로 비공개이므로, Users & Permissions 플러그인 또는 API 토큰을 통해 접근 권한을 명시적으로 구성해야 합니다. 이 스킬은 Cursor, Claude Code, Codex 등 다양한 AI 에이전트 플랫폼에서 사용할 수 있도록 설계되었습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/agents-inc/skills/tree/HEAD/src/skills/api-cms-strapi

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Strapi 패턴은(는) 무엇을 하나요?

빠른 가이드: Strapi를 오픈소스 헤드리스 CMS로 사용하여 콘텐츠 타입 스키마로부터 REST/GraphQL API를 자동 생성하세요. v5에서는 Entity Service API 대신 Document Service API(strapi.documents())를 백엔드 데이터 접근에 사용합니다. REST API 응답은 data.fieldName 형식(중첩된 data.attributes.fieldName 아님)을 사용합니다. 관계형 데이터와 미디어는 기본적으로 채워지지 않습니다. 항상 populate를 전달하세요. qs를 사용하여 복잡한 쿼리 문자열을 구성하세요. 콘텐츠 타입은 기본적으로 비공개입니다. Users & Permissions 플러그인 또는 API 토큰을 통해 권한을 구성하세요.


<critical_requirements>

중요: 이 스킬을 사용하기 전에

모든 코드는 CLAUDE.md의 프로젝트 규칙을 따라야 합니다 (kebab-case, 명명된 내보내기, import 순서, import type, 명명된 상수)

(Strapi v5의 모든 백엔드 데이터 접근에는 strapi.documents('api::content-type.content-type') (Document Service API)를 반드시 사용해야 합니다. Entity Service API는 제거되었습니다)

(관계형 데이터, 미디어, 컴포넌트 또는 동적 영역이 필요할 때는 항상 populate를 전달해야 합니다. Strapi는 기본적으로 관계형 데이터를 반환하지 않습니다)

(qs 라이브러리를 사용하여 필터, populate, 정렬을 포함한 복잡한 REST API 쿼리 문자열을 구성해야 합니다. 수동 문자열 구성은 중첩된 매개변수에서 문제를 일으킵니다)

(커스텀 컨트롤러에서 this.sanitizeQuery(ctx), this.sanitizeOutput(), this.validateQuery(ctx)를 사용하여 입력과 출력을 모두 삭제 및 검증해야 합니다)

(모든 콘텐츠 타입 엔드포인트에 대한 권한을 관리자 패널이나 구성을 통해 설정해야 합니다. 모든 라우트는 기본적으로 비공개입니다)

</critical_requirements>


자동 감지: Strapi, strapi, @strapi/strapi, createCoreController, createCoreService, createCoreRouter, Document Service, strapi.documents, content-type, schema.json, api::, plugin::, lifecycle hooks, Users & Permissions, /api/auth/local, populate, qs.stringify

사용 시기:

  • Strapi를 헤드리스 CMS로 사용하여 콘텐츠 관리 애플리케이션 구축
  • 필드, 관계, 컴포넌트, 동적 영역이 포함된 콘텐츠 타입 스키마(schema.json) 정의
  • 필터, populate, 정렬, 페이지네이션을 사용한 REST API 쿼리
  • 커스텀 컨트롤러, 서비스, 라우트, 정책 또는 미들웨어 생성
  • 초안/게시 워크플로우가 포함된 Document Service API를 사용한 백엔드 CRUD
  • Users & Permissions 플러그인을 사용한 JWT 인증 구현
  • 부작용을 위한 콘텐츠 타입 라이프사이클 훅 추가
  • 콘텐츠 스키마에 대한 TypeScript 타입 생성

주요 패턴:

  • 콘텐츠 타입 스키마 정의 (schema.json)
  • qs를 사용한 REST API 쿼리 (필터, populate, 정렬, 페이지네이션)
  • Document Service API (findMany, findOne, create, update, delete, publish, unpublish)
  • 커스텀 컨트롤러, 서비스, 라우트, 정책, 미들웨어
  • 라이프사이클 훅 (beforeCreate, afterUpdate 등)
  • JWT 인증 (등록, 로그인, 인증된 요청)
  • TypeScript 타입 생성

사용하지 말아야 할 때:

  • Strapi가 아닌 CMS 플랫폼 (해당 CMS 전용 스킬 사용)
  • Strapi의 API 계층을 우회하는 직접 데이터베이스 쿼리 (Strapi의 Document Service 사용)
  • 원시 SQL이 필요한 복잡한 트랜잭션 로직 (Strapi는 데이터베이스를 추상화)

상세 리소스:

  • 결정 프레임워크 및 빠른 참조 표는 reference.md를 참조하세요.

코어 및 REST API:

  • examples/core.md -- 콘텐츠 타입 스키마, REST API 쿼리, Document Service API, 오류 처리

백엔드 커스터마이징:

  • examples/backend.md -- 커스텀 컨트롤러, 서비스, 라우트, 정책, 미들웨어, 라이프사이클 훅, Document Service 미들웨어

인증:


철학

Strapi는 Node.js(Koa) 기반의 오픈소스 헤드리스 CMS로, 콘텐츠 타입 스키마로부터 RESTful 및 GraphQL API를 자동 생성합니다. 콘텐츠는 JSON 스키마를 통해 정의되고, 관리자 패널을 통해 관리되며, 생성된 API 엔드포인트를 통해 사용됩니다.

핵심 원칙:

  1. 스키마 기반 콘텐츠 -- 콘텐츠 타입은 필드, 관계, 컴포넌트 및 동적 영역을 설명하는 schema.json 파일에 정의됩니다. 관리자 패널 콘텐츠 타입 빌더는 시각적 편집기를 제공하지만, 스키마는 저장소에 있는 코드입니다.
  2. 자동 생성 API -- 모든 콘텐츠 타입은 자동으로 CRUD REST 엔드포인트(/api/:pluralApiId)와 선택적 GraphQL 지원을 얻습니다. 표준 작업을 위한 수동 라우트/컨트롤러 생성이 필요 없습니다.
  3. Document Service API (v5) -- 커스텀 코드, 플러그인 및 라이프사이클 훅에서 콘텐츠에 접근하기 위한 백엔드 API입니다. v4의 Entity Service를 대체합니다. 기본 식별자로 데이터베이스 id 대신 documentId를 사용합니다.
  4. 초안 및 게시 -- 콘텐츠 타입은 초안/게시 워크플로우를 가질 수 있습니다. Document Service는 기본적으로 status: 'draft'입니다. 게시된 콘텐츠는 status: 'published' 또는 명시적인 publish() 호출이 필요합니다.
  5. 권한 우선 -- 모든 콘텐츠 타입 엔드포인트는 기본적으로 비공개입니다. 접근 권한은 관리자 패널(Users & Permissions 플러그인) 또는 API 토큰을 통해 명시적으로 부여되어야 합니다.
  6. 백엔드 커스터마이징 -- 컨트롤러, 서비스, 라우트, 정책 및 미들웨어를 모두 사용자 정의할 수 있습니다. Strapi는 Koa를 기반으로 구축된 MVC와 유사한 패턴을 따릅니다.

코어 패턴

패턴 1: 콘텐츠 타입 스키마

콘텐츠 타입은 ./src/api/[api-name]/content-types/[content-type-name]/schema.json의 schema.json 파일에 정의됩니다. 다중 문서 콘텐츠(기사, 제품)에는 collectionType을 사용하고, 단일 문서 콘텐츠(사이트 설정, 홈페이지)에는 singleType을 사용합니다.

{
  "kind": "collectionType",
  "info": {
    "singularName": "article",
    "pluralName": "articles",
    "displayName": "Article"
  },
  "options": { "draftAndPublish": true },
  "attributes": {
    "title": { "type": "string", "required": true },
    "slug": { "type": "uid", "targetField": "title" },
    "author": {
      "type": "relation",
      "relation": "manyToOne",
      "target": "api::author.author",
      "inversedBy": "articles"
    },
    "blocks": {
      "type": "dynamiczone",
      "components": ["blocks.hero", "blocks.rich-text"]
    }
  }
}

주요 필드: uid는 targetField에서 슬러그를 자동 생성하고, relation 타입은 inversedBy/mappedBy로 카디널리티를 정의하며, component는 재사용 가능한 블록을 포함하고, dynamiczone은 혼합 컴포넌트 타입을 허용합니다. 전체 컬렉션 및 단일 타입 예제는 examples/core.md를 참조하세요.


패턴 2: qs를 사용한 REST API 쿼리

REST API는 필터링, 채우기, 정렬 및 페이지네이션을 위한 복잡한 쿼리 매개변수를 허용합니다. 항상 qs 라이브러리를 사용하여 쿼리 문자열을 구성하세요.

import qs from "qs";

const query = qs.stringify(
  {
    filters: { publishedAt: { $notNull: true } },
    populate: {
      author: { fields: ["name"] },
      categories: { fields: ["name", "slug"] },
    },
    sort: ["publishedAt:desc"],
    pagination: { page: 1, pageSize: 25 },
  },
  { encodeValuesOnly: true },
);
const response = await fetch(`${STRAPI_URL}/api/articles?${query}`);
const { data, meta } = await response.json();

핵심 사항: encodeValuesOnly는 대괄호 인코딩 문제를 방지하고, 필터는 연산자($eq, $notNull, $containsi, $or, $and)를 사용하며, fields와 함께 대상 populate를 사용하면 과도한 데이터 가져오기를 방지합니다 (프로덕션에서는 populate=*를 사용하지 마십시오). 동적 영역 컴포넌트는 on 구문을 사용합니다. 전체 필터링, 채우기 및 페이지네이션 예제는 examples/core.md를 참조하세요.


패턴 3: Document Service API (백엔드)

Document Service API는 커스텀 컨트롤러, 서비스, 라이프사이클 훅 및 플러그인에서 서버 측의 콘텐츠에 접근하는 데 사용됩니다. v4의 Entity Service를 대체합니다 (v5에서 제거됨).

const CONTENT_TYPE_UID = "api::article.article";

// 게시된 콘텐츠 찾기 (기본값은 'draft')
const articles = await strapi.documents(CONTENT_TYPE_UID).findMany({
  status: "published",
  filters: { categories: { slug: { $eq: "news" } } },
  populate: { author: true },
});

// CRUD는 데이터베이스 id 대신 documentId를 사용
const article = await strapi
  .documents(CONTENT_TYPE_UID)
  .findOne({ documentId });
const created = await strapi
  .documents(CONTENT_TYPE_UID)
  .create({ data: { title: "New" } });
await strapi.documents(CONTENT_TYPE_UID).publish({ documentId });

핵심 사항: strapi.documents(uid)는 strapi.entityService를 대체합니다 (v5에서 제거됨). documentId는 로케일 및 초안/게시 버전 전체에서 지속되는 식별자입니다. 기본 상태는 'draft'입니다 (명시적으로 status: 'published'를 전달하세요). 전용 publish()/unpublish()/discardDraft() 메서드가 있습니다. 전체 CRUD 및 게시/게시 취소 예제는 examples/core.md를 참조하세요.


패턴 4: 커스텀 컨트롤러

자동 생성된 컨트롤러 작업을 확장하거나 대체합니다. 컨트롤러는 요청/응답 로직을 처리하고 서비스에 위임합니다. factories의 createCoreController를 사용하여 삭제 헬퍼를 상속받으세요.

// src/api/article/controllers/article.ts
export default factories.createCoreController(
  "api::article.article",
  ({ strapi }) => ({
    async find(ctx) {
      await this.validateQuery(ctx);
      const sanitizedQuery = await this.sanitizeQuery(ctx);
      const { results, pagination } = await strapi
        .service("api::article.article")
        .find(sanitizedQuery);
      const sanitizedResults = await this.sanitizeOutput(results, ctx);
      return this.transformResponse(sanitizedResults, { pagination });
    },
  }),
);

이점: validateQuery + sanitizeQuery + sanitizeOutput은 역할 기반 필드 접근을 강제하고, transformResponse()는 출력을 { data, meta } 래퍼로 감쌉니다.

findBySlug, incrementViews, search와 같은 사용자 정의 작업을 포함한 전체 컨트롤러 예제는 examples/backend.md를 참조하세요.


패턴 5: 커스텀 라우트

사용자 정의 컨트롤러 작업을 노출하기 위해 커스텀 라우트를 정의합니다. 커스텀 라우트 파일은 코어 라우터와 함께 위치합니다.

// src/api/article/routes/custom-article.ts
export default {
  routes: [
    {
      method: "GET",
      path: "/articles/slug/:slug",
      handler: "api::article.article.findBySlug",
      config: { auth: false },
    },
  ],
};

이점: 코어 라우터와 별도 파일, handler는 전체 UID 사용, auth: false는 공개 접근 허용, 정책/미들웨어는 라우트별로 연결 가능

only, 미들웨어 연결 및 정책 구성을 사용한 라우트 제한 예제는 examples/backend.md를 참조하세요.


패턴 6: 라이프사이클 훅

콘텐츠 타입 작업에 부작용을 등록합니다. 훅은 콘텐츠 타입 스키마와 함께 lifecycles.ts 파일에 정의됩니다.

// src/api/article/content-types/article/lifecycles.ts
export default {
  async beforeCreate(event) {
    /* event.params.data -- 입력을 변환하기 위해 변경 */
  },
  async afterCreate(event) {
    /* event.result -- 생성된 문서 */
  },
  async beforeUpdate(event) {
    /* event.params.data -- 저장 전에 변경 */
  },
  async afterDelete(event) {
    /* event.result -- 관련 리소스 정리 */
  },
};

이점: 훅은 Document Service 작업 시 자동으로 실행되며, beforeXxx는 event.params.data를 변경할 수 있고, afterXxx는 event.result를 가집니다.

참고: v5의 교차 관심사(감사 로깅, 캐시 무효화)의 경우 src/index.ts에 등록된 Document Service 미들웨어(strapi.documents.use((ctx, next) => { ... }))를 선호하세요. 라이프사이클 훅은 콘텐츠 타입별 부작용에 여전히 사용 가능합니다.

전체 라이프사이클 예제(슬러그 생성, 프로그래밍 방식 구독, 감사 로깅 포함)는 examples/backend.md를 참조하세요.


패턴 7: 서비스

서비스는 컨트롤러가 호출하는 재사용 가능한 비즈니스 로직을 포함합니다. createCoreService를 사용하여 기본 CRUD를 상속받고 사용자 정의 메서드를 추가합니다.

export default factories.createCoreService(
  "api::article.article",
  ({ strapi }) => ({
    async findPublished(filters = {}) {
      return strapi
        .documents("api::article.article")
        .findMany({ status: "published", filters });
    },
  }),
);
// 호출: strapi.service("api::article.article").findPublished()

이점: 기본 CRUD 상속, 사용자 정의 메서드는 재사용 가능한 쿼리 로직을 캡슐화하며, 컨트롤러에서 strapi.service(uid)를 통해 호출됩니다.

관련 기사 쿼리 및 게시-알림 패턴을 포함한 복잡한 서비스 예제는 examples/backend.md를 참조하세요.


패턴 8: 정책

정책은 라우트에 대한 접근을 허용하거나 거부하는 읽기 전용 함수입니다. true (허용) 또는 false (거부)를 반환하며 요청을 수정할 수 없습니다.

// src/api/article/policies/is-owner.ts
export default async (policyContext, config, { strapi }) => {
  const article = await strapi.documents("api::article.article").findOne({
    documentId: policyContext.params.documentId,
    populate: { author: true },
  });
  return article?.author?.documentId === policyContext.state.user?.documentId;
};

이점: 읽기 전용 검사 (요청 변경 없음), 코어 라우터 구성에서 작업별로 적용됩니다.

라우트에 정책 적용, 속도 제한 정책 및 다중 정책 연결 예제는 examples/backend.md를 참조하세요.


<decision_framework>

결정 프레임워크

REST API vs Document Service API

코드가 실행되는 위치는 어디인가요?
+-- 클라이언트 측 (브라우저, 프론트엔드 앱)
|   +-- REST API 사용 (fetch /api/:pluralApiId)
+-- 서버 측 (커스텀 컨트롤러, 서비스, 플러그인, 라이프사이클 훅)
    +-- Document Service API 사용 (strapi.documents(uid))

채우기 전략

관련 데이터가 필요합니까?
+-- 아니요 --> populate를 전달하지 마세요 (더 가벼운 응답)
+-- 예 --> 어느 수준의 제어가 필요합니까?
    +-- 모든 관계, 1레벨 --> populate: '*' (편리하지만 과도하게 가져옴)
    +-- 특정 관계 --> populate: { relation: { fields: [...] } }
    +-- 중첩 관계

…

[번역을 위해 잘림]

Individual skills in this repo

This repo contains 11 individual skills — each has its own dedicated page.

관련 스킬