KuroCMS 개발기 Vol.5: 레시피 전용 타입 기능 구현
사용자 피드백을 바탕으로 KuroCMS에 새로운 레시피 카드 기능을 구현했습니다. 기술 아키텍처, JSON-LD 생성, 그리고 에디터 통합의 자세한 세부 사항을 알아보세요.
엣지(Edge) 환경에서 작동하는 차세대 헤드리스 CMS로 개발 중인 'KuroCMS' 및 'KuroEditor'의 최신 버전에서 새로운 '레시피 카드' 기능이 공식적으로 추가 구현되었습니다. 기존의 KuroCMS는 마크다운이나 표준 HTML을 사용한 범용 문서 표현에 주로 초점을 맞추었으나, 이번 레시피 카드의 도입을 통해 조리 시간, 재료 목록, 인원, 조리 단계 등의 메타데이터를 엄격하게 처리하여 구글 등 검색엔진이 이해하기 쉬운 구조화 데이터(Schema.org Recipe)의 일관된 출력을 보장할 수 있게 되었습니다. 본 문서에서는 이 레시피 카드가 어떤 기술 설계하에 개발되었으며 에디터, API, 프론트엔드 레이어 간에 어떻게 검증이 철저히 집행되는지 상세 아키텍처와 구현 이면을 자세히 소개해 드립니다.
설계 사상과 기본 설계: 1개 문서 = 1개 레시피로 제한한 스마트 데이터 모델
레시피 카드를 구현하면서 개발팀이 직면한 가장 큰 과제는 '데이터 일관성과 중복 배제'였습니다. 일반적인 레시피 게시 사이트에서는 한 문서에 여러 개의 레시피 카드가 흩어져 있는 경우가 많지만, 이는 검색엔진 구조화 데이터(JSON-LD) 관점에서 보면 매우 불량한 구조를 생성하기 쉽습니다. 왜냐하면 단일 URL(문서)에 대해 여러 개의 서로 다른 요리나 조리 단계가 병렬로 출력되면 문서의 공통 속성(예: 요리명, 완성 이미지, 설명, 발행일 등)과 개별 레시피 카드 내부 데이터 간의 대응 관계가 모호해지기 때문입니다.
따라서 KuroCMS에서는 '1개 문서 = 1개 레시피'의 규칙적 제약을 강제합니다. 레시피 카드(RecipeCard)는 인원, 조리 시간, 재료, 단계 등의 '레시피 전용 가변 부분'만 담당하고 요리명, 설명, 완성 이미지, 발행일 등의 공통 정보는 문서의 속성(제목, 요약, 커버 이미지, 생성일 등)을 재사용하도록 설계했습니다. 이를 통해 중복 입력을 원천 방지하고 동일한 요리 이름이 동일한 구조화 데이터 내에서 깨진 포맷으로 무한 루프를 도는 문제를 방지합니다.
솔직히 KuroEditor의 디자인 관점에서는 레이아웃의 유연성을 저해하는 1개 문서 = 1개 레시피 제약을 크게 환영하지는 않았습니다. 그러나 사용자의 레시피 페이지가 더 높은 SEO 평가를 얻고 읽기 및 참조가 더 쉬워지므로 현재 버전에서는 문서당 1개의 레시피 카드 규칙을 엄격하게 준수합니다.
공유 순수 함수를 통한 일관성 보장
헤드리스 CMS 환경에서 리치 에디터(KuroEditor), 저장용 API, 그리고 공개 페이지의 정적 빌드 프로세스 세 군데에서 완전히 동일한 데이터 구조 검증(Validation)을 수행하는 것은 쉬운 일이 아닙니다. 각각의 레이어에서 다른 언어나 프레임워크를 사용해 개별 검증 로직을 작성하면 구현상의 갭으로 인해 '에디터에서는 저장 성공했으나 API 서버에서 에러가 남', 혹은 '데이터베이스에는 저장되었으나 프론트엔드 빌드 시 에러가 발생해 웹사이트가 깨짐'과 같은 불일치가 반드시 일어납니다.
이 문제를 근본적으로 해결하기 위해 KuroCMS는 '공유 순수 함수(Shared Pure Function)' 디자인 패턴을 도입했습니다. 구체적으로 upstream인 KuroEditor 저장소에서 빌드된 공통 레시피 처리 모듈인 'dist/kuro-recipe.js'를 KuroCMS 코어의 'src/kuro-recipe.js'로 벤더인(내포)하고 타입 정의 파일 'kuro-recipe.d.ts'와 함께 배치했습니다. 이를 통해 에디터 편집 시, 저장 API 호출 시, 정적 빌드 시 완전히 동일한 JavaScript 코드가 동일한 검증 기준으로 본문 HTML 내 레시피 데이터를 검사합니다. 단일 광원(Single Source of Truth) 설계가 버그를 최소화해 줍니다.
*KuroEditor와 KuroCMS 모두 Kuroboo가 개발하고 있는 '오픈 소스 소프트웨어'이므로 GitHub 히스토리를 함께 참고하시면 훨씬 이해하기 쉬울 것입니다.
저장 API의 엄격한 검증: recipe-guard.ts 규격
KuroCMS에 문서를 저장(PUT)할 때 서버사이드 API는 단순히 문자열을 저장하는 것뿐만 아니라, 전송된 본문 HTML 내에 레시피 데이터가 올바르게 내포되어 있는지 엄격하게 검사합니다. 이 역할은 백엔드 'src/recipe-guard.ts'의 'checkRecipeCards(bodyHtml, isRecipeType)' 함수가 담당합니다.
이 검증에서 가장 중요한 점은 검증 수행 타이밍입니다. KuroCMS는 여러 사용자의 협업 편집을 고려하고 있으며 최종 저장 데이터는 여러 버전에 대한 '3방향 병합(3-way merge)'을 거쳐 데이터베이스에 기록됩니다. 병합 전에 검증을 해버리면 병합 과정에서 우연히 레시피 카드가 두 개로 복제되거나 깨진 데이터가 삽입되는 경우를 막을 수 없습니다. 따라서 검증은 반드시 병합이 완료된 최종 본문에 대해 실행됩니다. 검증에 실패하면 API는 클라이언트에 명확한 에러를 반환해 저장 프로세스를 차단합니다.
| 검사 항목 | 구체적인 검사 내용 및 보안 대책 |
|---|---|
| 레시피 카드 개수 | 레시피 문서에는 반드시 정확히 1개의 카드가 존재하며, 비레시피 문서에는 반드시 0개의 카드가 존재함을 확인합니다. |
| data-recipe 복호화 | HTML 태그에 포함된 인코딩된 레시피 JSON 데이터를 복호화하여 Schema 규격에 맞는지 검사합니다. |
| 허용 목록(allowlist) | 알 수 없는 HTML 속성이 섞여 있는지 엄격하게 체크합니다. 이를 통해 악의적 사용자가 HTML 본문을 거쳐 'onclick' 등의 XSS 스크립트를 삽입하려는 보안 취약점을 완전히 제거합니다. |
| 버전 호환성 확인 | 레시피 Schema의 버전 호환성을 확인하여 구버전 카드 데이터나 손상된 형식이 저장되는 것을 차단합니다. |
JSON-LD 출력 규격: SEO 효과를 극대화하는 Recipe 구조 자동 생성
이 부분이 레시피 카드 추가의 가장 핵심 가치입니다. 보통 KuroCMS는 페이지의 구조화 데이터로 'Article'을 출력하지만, 문서에 레시피 카드가 존재할 때는 이를 자동으로 교체하여 'Recipe'를 주요 구조화 데이터로 출력해 줍니다.
저장된 HTML에서 'data-recipe' 속성을 읽어 파싱한 데이터를 'article.recipe' 및 'page.isRecipe' 변수로 HTML 템플릿에 전달합니다. 이때 템플릿단에서 '문자열 비교를 통한 타입 체크'를 피하고자 단순 불리언 분기(#if page.isRecipe)를 사용해 템플릿을 전환합니다. 시간 데이터는 내부적으로 국제 표준인 ISO-8601 duration 포맷(예: 25분이면 'PT25M')으로 자동 변환되어 출력됩니다. HTML 디스플레이용과 JSON-LD 구조화 데이터용 소스가 완전히 동일한 단일 소스에서 생성되므로 검색엔진 정보 미스매치 위험이 전혀 없습니다.
어드민 통합: 동적 에디터 재구성 로직
관리자 패널의 에디터 영역에도 고도의 연동 로직이 내장되었습니다. KuroEditor에서 레시피 생성을 위한 '냄비 버튼' 활성화 여부는 초기화 시 전달되는 'recipeUi' 옵션으로 제어합니다. KuroEditor의 기본 값은 버튼 표시(true)이므로 KuroCMS에서는 특별히 지정해 줄 필요가 없으나, false로 지정하면 레시피 편집에 KuroEditor를 사용하지 않는 유저에게는 아무것도 변하지 않은 것처럼 보이므로 변경의 부작용을 최소화할 수 있습니다.
또한 KuroEditor 레벨에서 레시피 카드는 문서당 1개로 하드록(두 번째 버튼은 비활성화되어 클릭 불가)됩니다. 이를 통해 레시피 문서의 핵심 규칙이 지켜집니다. KuroCMS 역시 저장 시 더블 체크를 돌려 레시피 문서에 레시피 전용 검증이 정상 작동하도록 제어합니다.
이 규격에 기초하여 레시피 관련 테스트 스위트인 'npm run test:recipe'(src/recipe-guard.test.ts, 총 12개 테스트 케이스)가 추가되었으며, 카드 수 확인부터 속성 내 특수문자 이스케이프 판정까지 견고하게 검증하고 있습니다.
남은 과제와 향후 개발 로드맵
이번 업데이트를 통해 레시피 문서의 '코어 시스템' 및 '저장 검증'은 완전히 작동 상태에 들어갔습니다. 다만 KuroCMS가 대형 포털 사이트 같은 거대한 레시피 레이아웃을 제공하는 것은 아닙니다. 우리의 목표는 개인 사이트에서 레시피를 손쉽게 게시하고 유저가 직접 코딩할 필요 없이 검색엔진 친화적인 SEO 데이터가 자동 생성되도록 돕는 것이었습니다. 이제 누구나 쉽게 요리 사이트를 런칭할 수 있습니다.
또한 KuroCMS는 템플릿 시스템이 탑재되어 있으므로 유저 여러분이 직접 디자인을 개선할 수도 있습니다(자세한 내용은 출처 3 참고). KuroCMS를 더욱 사용하기 편리하게 개선하기 위해 개발팀은 앞으로도 여러분의 피드백을 기반으로 업데이트를 진행할 예정이니 많은 응원 부탁드립니다!