전체 검색

글, 태그와 카테고리를 검색하고 바로 이동해요

테마를 코드가 아니라 데이터로 만들기

색만 바뀌는 건 테마가 아니라 팔레트더라고요. 글꼴도 형태도 컴포넌트 생김새도 바뀌게 만들면서, 사용자가 만든 테마에 자바스크립트 한 줄도 허용하지 않은 이야기예요.

2026. 07. 29. 10:08:447분 읽기프로젝트/arcaC/아키텍처
#아키텍처
조회 21
같은 형태의 그릇이 서로 다른 유약으로 구워져 놓인 도자기 공방과 배합표 일러스트

테마를 바꿨는데 색만 바뀌더라고요.

밝게·어둡게가 있고, 강조색을 고를 수 있고, 모서리 곡률도 조절됐어요. 나쁘지 않죠.

그런데 사이트 두 개를 나란히 놓으면 그냥 같은 사이트에 물감만 다르게 칠한 것 같았습니다. 그건 테마가 아니라 팔레트예요.

원하는 건 이거였어요

가족마다 사이트가 다른 인상을 가졌으면 했어요. 복고풍, 사이버, 클래식, 잔잔한 것, 어두운 것, 발랄한 것.

그리고 이게 핵심인데 — 그 테마들이 특별한 코드 경로면 안 됐어요. 회사가 만든 테마와 사용자가 만든 테마가 같은 방식으로 굴러가야 했습니다. 안 그러면 "공유"라는 게 성립을 안 하거든요.

제가 만든 테마만 컴포넌트 생김새를 바꿀 수 있고 사용자 테마는 색만 바꿀 수 있다면, 그건 테마 시스템이 아니라 그냥 제 취향 모음집이죠.

팔레트와 테마의 차이

말장난처럼 들릴 수 있어서 표로 갈라 볼게요. 무엇까지 바뀌면 "다른 인상"이 되는가의 문제예요.

바뀌는 것팔레트테마
바뀜바뀜
모서리 곡률조금바뀜
글꼴과 자간안 바뀜바뀜
그림자와 재질안 바뀜바뀜
모션안 바뀜바뀜
버튼이 생긴 모양 자체안 바뀜바뀜

마지막 줄이 경계예요. 앞의 다섯은 값을 바꾸면 되는데, 마지막은 그리는 방법이 달라야 하거든요. 그래서 대부분의 테마 시스템이 여기서 멈춥니다.

그리고 이걸 넘으려면 "사용자가 그리는 방법을 정한다"가 되는데, 그 순간 보안 문제가 시작돼요. 이 글의 나머지는 사실상 그 이야기입니다.

제일 쉬운 길은 위험했습니다

사용자가 스타일을 자유롭게 쓰게 하면 가장 강력해요. CSS를 직접 적게 하거나, 작은 스크립트를 허용하거나요.

그리고 그 순간 이 시스템은 남의 코드를 우리 페이지에서 실행하는 시스템이 됩니다.

그래서 반대로 갔어요. 테마 데이터에는 실행되는 것이 하나도 안 들어갑니다. 자바스크립트도, 리액트 코드도, CSS 원문도, 외부 주소도요.

들어가는 건 등록된 이름과 검증된 설정값뿐이에요. 실제 스타일 시트는 엔진이 허용 목록을 보고 만들어 냅니다. 사용자는 재료를 고르는 거지 요리하는 게 아니에요.

(자유도를 줄인 게 아니라 자유도의 종류를 바꾼 거예요. 무엇이든 쓸 자유 대신, 안전하게 조합할 자유요.)

자유롭게 스타일을 쓰게 하면 뭐가 열리는지 적어 볼게요.

허용하면열리는 것그래서 안 함
CSS 원문선택자로 다른 요소까지 건드림발행된 남의 사이트에 스타일이 샘
작은 스크립트실행 시점에 뭐든 가능우리 도메인에서 남의 코드가 돎
외부 이미지 주소추적과 가용성이 남의 손에링크가 죽으면 사이트가 깨짐
임의 폰트 URL같은 문제같음

그래서 테마 데이터에 들어갈 수 있는 걸 뒤집어서 정의했어요. 허용 목록에 있는 것만 들어갑니다.

// 테마에 저장되는 것 — 전부 데이터입니다
{
  "engineVersion": "2026.7",
  "tokens":  { "radius.md": "10px", "font.body": "pretendard" },
  "recipes": { "button": "pill-solid", "card": "flat-outline" },
  "assets":  [{ "kind": "font", "id": "font-pretendard" }]
}

// 저장할 수 없는 것
// - JavaScript / React 코드
// - CSS 선택자나 속성 원문
// - 외부 모듈 URL, 관리되지 않는 원격 자산

실제 스타일 시트는 엔진이 이 데이터를 보고 만들어 냅니다. 사용자는 재료를 고르는 거지 요리하는 게 아니에요. 자유도를 줄인 게 아니라 자유도의 종류를 바꾼 겁니다.

그럼 컴포넌트 생김새는 어떻게 바꾸냐면

여기가 재미있는 부분이었어요. 색만 바꾸는 걸 넘으려면 버튼 자체가 다르게 생겨야 하잖아요.

그래서 컴포넌트마다 "이 컴포넌트는 의미상 무엇이고 접근성상 무엇을 보장한다"는 계약을 먼저 선언했습니다. 그 계약이 있는 컴포넌트에 한해, 테마가 어떤 조리법으로 그릴지를 고를 수 있어요.

새로운 조리법을 추가하는 건 테마 데이터가 아니라 검수된 확장으로만 됩니다. 그러니까 테마는 "이 중에서 고른다"이고, 목록을 늘리는 건 다른 통로예요.

그리고 선은 하나 그었습니다. 테마는 생김새를 바꾸지 화면의 구조나 정보 배치는 못 바꿔요. 그건 페이지가 소유합니다. 테마가 정보 구조까지 건드리기 시작하면 테마를 바꿨을 뿐인데 내용이 사라지는 일이 생기거든요.

그래서 컴포넌트마다 계약을 먼저 선언했어요.

// 이 컴포넌트가 무엇이고, 무엇을 반드시 보장하는가
defineThemeableComponent("button", {
  semantics: "사용자가 눌러서 동작을 실행한다",
  guarantees: [
    "포커스 링이 보인다",
    "비활성 상태가 색 외의 방법으로도 드러난다",
    "레이블 텍스트가 요소 안에 존재한다",
  ],
  recipes: ["pill-solid", "flat-outline", "raised-classic"],
});

// 테마는 recipes 중에서 고릅니다.
// 목록을 늘리는 건 검수된 확장의 몫이에요.
테마가 할 수 있는 것테마가 못 하는 것
등록된 조리법 중 고르기새 조리법 만들기
토큰 값 바꾸기토큰 이름 새로 만들기
등록된 자산 참조외부 주소 참조
생김새 바꾸기정보 구조·배치 바꾸기

오른쪽 마지막 줄이 이 설계에서 제일 중요한 선이에요. 테마가 정보 구조까지 건드리기 시작하면, 테마를 바꿨을 뿐인데 내용이 사라지는 일이 생깁니다.

그리고 접근성 보장은 조리법이 아니라 계약에 붙어 있어요. 어떤 조리법을 고르든 포커스 링은 보여야 합니다. 예쁜 테마를 고른 대가로 키보드 사용자가 길을 잃으면 안 되니까요.

발행하면 그 순간의 테마가 굳습니다

이건 이 플랫폼의 오래된 습관이에요. 공개된 것은 잠긴다.

사이트를 발행하면 그 시점의 테마 결과물이 통째로 잠깁니다. 나중에 제가 그 테마를 고치거나 지워도, 이미 공개된 사이트는 안 바뀌어요.

안 그러면 이런 일이 생기잖아요. 남이 만든 테마를 쓰고 있는데 그 사람이 테마를 바꾸는 바람에 내 사이트가 어느 날 갑자기 달라지는 것.

(테마를 가져다 쓰는 건 구독이 아니라 복사예요. 업데이트하고 싶으면 직접 눌러야 합니다.)

그리고 상속을 안 만든 것도 의도예요. 이게 흔한 갈림길이거든요.

방식장점문제
테마가 다른 테마를 상속중복이 적음부모가 바뀌면 자식이 갑자기 달라짐
포크(복사) + 출처만 기록예측 가능중복이 생김

두 번째를 골랐어요. 남의 테마를 가져다 쓰는 건 구독이 아니라 복사입니다. 원본이 바뀌어도 내 사이트는 안 바뀌고, 업데이트하고 싶으면 직접 눌러야 해요.

그리고 발행하면 그 시점의 결과물이 통째로 잠깁니다. 테마 스냅샷, 엔진 버전, 필요한 스타일 시트와 자산까지 릴리스 안에 묶여요.

이 플랫폼에서 계속 반복되는 습관이에요. 공개된 것은 잠긴다. 초안은 얼마든지 흔들려도 되지만, 남이 보고 있는 것은 흔들리면 안 되니까요.

그리고 편집 화면이 안 흔들리게

실무적으로 제일 성가셨던 문제를 하나 적을게요.

빌더에서 테마를 고르면 미리보기가 바뀌어야 하는데, 편집 도구 자체까지 같이 바뀌면 안 돼요. 어두운 테마를 고르는 순간 버튼이 안 보여서 되돌릴 수도 없게 되면 곤란하잖아요.

그래서 편집 도구는 플랫폼 테마를 유지하고, 사이트 테마는 미리보기 영역 안에만 칠합니다. 두 세계가 한 화면에 있는 거죠.

비슷한 이유로 브랜드 팔레트와 화면 밝기도 갈랐어요. 브랜드 색을 골랐다고 해서 사용자가 선택한 밝게·어둡게가 강제로 바뀌면 안 되니까요.

두 세계를 한 화면에 두는 건 이렇게 풀었어요.

/* 편집 도구는 플랫폼 테마를 유지합니다 */
.studio-chrome { /* platform theme */ }

/* 사이트 테마는 미리보기 안쪽에만 */
.preview-canvas[data-site-theme] { /* site theme */ }

/* 그리고 미리보기에서 뜨는 팝업도 같은 범위 안에 */
.preview-portal[data-site-theme] { /* site theme */ }

세 번째 줄이 실제로 제일 자주 놓치는 부분이었어요. 미리보기 안에서 연 팝업은 DOM 상으로는 화면 맨 바깥에 붙거든요. 그러면 사이트 테마가 안 먹고 혼자 플랫폼 테마로 뜹니다.

비슷한 이유로 브랜드 색과 화면 밝기도 갈랐어요. 브랜드 색을 골랐다고 사용자가 선택한 밝게·어둡게가 강제로 바뀌면 안 되니까요. 둘은 다른 축이에요.

아직 안 끝났습니다

솔직히 이건 비용이 큰 결정이었어요. 테마마다 이력과 공개본을 관리해야 하고, 컴포넌트마다 계약과 검증이 필요합니다.

그런데 이걸 안 하면 "사용자가 사이트를 직접 꾸민다"는 이 플랫폼의 원래 목적이 성립을 안 해요. 이 플랫폼을 만든 이유가 가족이 직접 자기 사이트를 만지게 하는 거였거든요.

직접 만든다는 말은 곧 남의 것을 가져다 고칠 수 있다는 뜻이에요. 그걸 안전하게 만들려면 테마가 코드가 아니라 데이터여야 했습니다.

치른 값

치른 것왜 받아들였나
테마마다 이력·공개본 관리저장할 게 늘어남 — 대신 발행된 사이트가 남의 수정에 안 흔들림
컴포넌트마다 계약 선언새 컴포넌트를 테마 대상으로 만들 때 절차가 붙음 — 접근성이 조리법과 무관하게 지켜짐
조리법을 검수 통로로만 추가즉시 확장이 안 됨 — 실행 코드가 데이터에 안 섞임
상속 대신 복사같은 값이 여러 테마에 중복됨 — 예측 가능성과 맞바꿈

코어 설계 이야기는 여기까지예요. 공개 전엔 안 바뀌는 선, 플러그인이 DB를 모르는 경계, 한눈에 보는 구조, 그리고 코드 없는 테마까지 — 전부 같은 원칙의 다른 얼굴입니다.

시리즈 이어읽기

플랫폼 코어 설계

4 / 4

이야기 나누기

반응을 남기고 새 글 소식을 받아보세요.

댓글0
서로를 존중하는 대화를 남겨주세요.

첫 댓글을 남겨 대화를 시작해보세요.

새 글 구독
새 글이 공개되면 이메일로 알려드려요.