전체 검색

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

별도 /edit 없이, 같은 URL에서 편집 모드로

보던 화면을 그대로 편집한다 — 그런데 로그인 세션은 다른 도메인에 있었어요. 복제 화면 2,000줄을 만들었다 지우고, __Host- 쿠키와 게이트웨이로 같은 주소에서 편집 모드를 켠 기록입니다.

2026. 07. 14. 12:20:136분 읽기프로젝트/arcaC/기술 노트
#BFF#EditMode#Cloudflare Workers#code splitting
조회 50
같은 URL 편집 모드 썸네일

글을 하나 고치려고 편집 화면을 열었다가, 묘하게 낯선 화면과 마주쳤어요. 같은 글인데 같은 화면이 아니었죠.

이 플랫폼의 제품 철학 중 하나는 "보고 있는 사이트를 그대로 편집한다"예요. 관리자 페이지로 건너가서 목록에서 글을 찾는 방식 말고, 읽던 그 자리에서 바로 고치는 것.

그런데 배포하고 나니 벽이 있었습니다.

두 개의 도메인 문제

주소가진 것
플랫폼(관리)arcacore.com로그인 세션
블로그(공개)blog.arcacore.com고치고 싶은 글

브라우저는 당연히 다른 origin의 세션을 읽게 해주지 않아요. 보안상 그게 맞고요.

(옆 건물 출입증으로 이 건물 문이 안 열리는 것과 같아요.)

그러니까 blog.arcacore.com은 "지금 이 사람이 로그인했는지"를 알 방법이 없었습니다.

첫 번째 시도 — 복제 화면

급한 대로 플랫폼 안에 편집 전용 화면을 만들었어요. 플랫폼 도메인 안이니 세션 문제가 없고, 권한 경계도 이미 있었거든요.

동작은 했습니다. 그런데 곧 근본적인 문제가 보였어요.

편집 화면이 실제 공개 사이트와 분리된 복제 화면이 된 거예요. 공개 사이트가 바뀔 때마다 편집 화면도 같이 고쳐야 했습니다. 안 고치면 둘이 서서히 달라지고요.

그리고 이건 시간이 갈수록 나빠지는 종류의 부채예요. 화면이 늘어날수록 복제할 것도 늘어나니까요.

결국 이 방향을 접고 편집 라우트와 관련 코드 약 2,000줄을 통째로 지웠습니다.

(같은 날 오후에 공들여 복구한 코드를 두 시간 만에 지웠어요. 네, 아까웠습니다. 그래도 복제 화면을 계속 끌고 가는 비용보단 쌌어요.)

다른 길들도 다 위험했어요

방법왜 안 했나
토큰을 URL로 넘기기주소창·기록·리퍼러에 남음
postMessage로 토큰 전달받는 쪽 검증이 조금만 느슨해도 유출
도메인 공유 쿠키하위 도메인 전체가 토큰을 봄
공개 페이지에 편집기 통째로 싣기익명 방문자가 편집기 코드를 다운로드

앞의 셋은 공통점이 있어요. 토큰의 노출 범위가 넓어집니다. 편의를 위해 열쇠를 여러 곳에 복사하는 셈이죠.

결론 — 주소는 그대로, 세션만 옮긴다

같은 주소의 브라우저 창 두 개가 읽기 모드와 편집 모드로 나뉘고 사이를 __Host- 쿠키가 잇는 그림
URL은 콘텐츠 경로만 표현하고, 편집 여부는 세션과 권한이 정합니다.

별도 편집 주소를 만들지 않기로 했어요. 주소는 "어떤 콘텐츠인가"만 말하고, 편집 여부는 그 호스트에 있는 세션과 권한이 정합니다.

그래서 필요한 건 하나였어요. 플랫폼의 로그인 상태를 블로그 호스트의 세션으로 한 번만 옮기는 것.

쿠키 이름이 곧 정책입니다

const authoringCookieName        = "__Host-arcac-authoring";
const authoringRefreshCookieName = "__Host-arcac-authoring-refresh";

`__Host-` 접두사가 붙으면 브라우저가 규칙을 강제해요.

브라우저가 강제하는 것막는 사고
Secure 필수평문으로 새지 않음
Domain 지정 불가 (host-only)다른 하위 도메인이 못 봄
Path=/ 고정경로를 좁혀서 헷갈리게 만들 수 없음

규칙을 서버 코드가 아니라 쿠키 이름에 넣은 거예요. 우리가 실수해도 브라우저가 거절합니다.

설치는 좁게, 확인은 넓게

쿠키를 심는 경로와 상태를 확인하는 경로를 다르게 뒀어요.

설치 (POST)확인 (GET)
누가 부르나플랫폼 origin에서만아무나
필요한 것토큰쿠키
검증 실패 시거절만료 쿠키를 내려 스스로 정리

마지막 줄이 실무적으로 중요했어요. 죽은 세션이 남아 있으면 "로그인한 것 같은데 아무것도 안 되는" 상태가 되거든요. 그래서 확인 요청이 검증에 실패하면 조용히 쿠키를 지웁니다.

(실제로 이 글을 쓰는 세션에서도 만료를 만났어요. 설치는 플랫폼 origin에서만 되기 때문에, 블로그 쪽에서 직접 심으려던 시도가 403으로 막혔습니다. 설계대로 동작한 거죠.)

익명 방문자에게 편집기 코드는 0바이트

성능 쪽 원칙은 하나였어요. 익명 방문자의 초기 다운로드에 편집기 코드가 1바이트도 들어가면 안 된다.

그래서 편집기를 별도 앱으로 빌드하고, 세션이 확인된 뒤에만 불러옵니다.

// 익명 방문자가 받는 것
공개 페이지 HTML + 공개 런타임 JS
세션 확인 GET 1회 (쿠키 없으면 즉시 끝)

// 로그인한 사람이 추가로 받는 것
편집기 번들 (그때 처음 요청)

그리고 편집기가 로드돼도 새 페이지로 이동하지 않아요. 지금 그려진 화면 위에 편집 모드를 얹습니다. 그래서 읽던 위치도, 스크롤도 그대로예요.

세션을 어떻게 넘기나 — 실제 흐름

추상적으로만 말했으니 실제 순서를 적을게요.

1. 사용자가 arcacore.com 에 로그인   → 플랫폼 도메인에 세션 생성
2. blog.arcacore.com 방문
3. 블로그가 세션 확인 GET 호출        → 쿠키 없음 → 익명으로 렌더
4. 사용자가 플랫폼에서 "이 사이트 편집" 진입
5. 플랫폼 origin 에서 설치 POST 호출  → __Host- 쿠키 심김
6. 이제 블로그의 세션 확인 GET 이 통과 → 편집기 로드

5번이 이 설계의 관문이에요. 이 요청은 플랫폼 origin에서 온 POST만 통과합니다. 다른 곳에서 부르면 거절돼요.

그래서 공격자가 사용자를 유인해서 쿠키를 심게 만들 수 없습니다. 심으려면 이미 플랫폼에 로그인해 있어야 하니까요.

토큰은 쿠키로만 삽니다

한 가지 더 신경 쓴 게 있어요. 이 토큰은 자바스크립트가 읽을 수 없습니다.

HttpOnly로 심기 때문에 `document.cookie`로 안 보여요. 그래서 페이지에 스크립트 하나가 잘못 들어와도 토큰을 훔쳐갈 수 없습니다.

대신 불편한 점이 있어요. 우리 코드도 토큰 값을 못 읽습니다. 그래서 "지금 로그인 상태인가"를 알려면 매번 서버에 물어봐야 해요. 그 요청이 앞에서 말한 세션 확인 GET입니다.

(읽을 수 없는 열쇠가 안전한 열쇠예요. 우리도 못 읽는 게 정상입니다.)

그리고 게이트웨이가 하는 일

블로그 호스트에는 인증 서버가 없어요. 그럼 이 쿠키를 누가 검증할까요.

const authoringRootPath      = "/__arcac/authoring";
const authoringSessionPath   = `${authoringRootPath}/session`;
const authoringApiSitePrefix = `${authoringRootPath}/api/sites/`;

공개 Worker에 이 경로들이 있어요. 여기로 오는 요청은 게이트웨이가 받아서 API로 넘깁니다.

그러니까 블로그 호스트는 저작 기능을 구현하지 않고 전달만 해요. 권한 판정도, 데이터 변경도 전부 API 쪽에서 일어납니다.

이 구조 덕에 사이트가 몇 개로 늘어나도 저작 기능은 한 벌이에요. 캘린더도 같은 경로를 씁니다.

접두사가 왜 __arcac 인가

사소해 보이는 결정 하나를 적어둘게요. 저작용 경로에 전부 같은 접두사를 붙였어요.

이유는 충돌 방지예요. 이 플랫폼 위에는 어떤 사이트든 올라올 수 있고, 그 사이트가 `/session`이나 `/api` 같은 경로를 쓰고 싶을 수 있거든요.

플랫폼이 쓰는 경로를 한 접두사 아래로 모아두면, 사이트가 쓸 수 있는 주소 공간이 그만큼 넓어집니다. 플랫폼은 자기 자리를 최대한 좁게 차지해야 해요.

숨긴 버튼은 보안 경계가 아닙니다

여기서 착각하기 쉬운 게 있어요. 편집 버튼이 안 보이면 안전한 것 같잖아요.

아니에요. 화면에 버튼이 없어도 요청은 만들 수 있습니다. 그래서 이 플랫폼의 불변 조건 중 하나가 이거예요.

숨긴 버튼은 보안 경계가 아니다.

최종 권한 판정은 여전히 서버의 명령 경계에서 일어납니다. 쿠키는 그 판정을 요청할 자격일 뿐이고, 판정 자체가 아니에요.

그래서 편집 UI가 떠 있어도 권한이 없으면 저장이 거절됩니다. 화면은 편의고 경계는 서버에 있습니다.

치른 값

불편해진 것실제로 겪은 일
호스트마다 세션 핸드셰이크가 따로블로그와 캘린더에 각각 심어야 함
로그아웃도 호스트별한 곳에서 나가도 다른 곳은 남아 있음
세션 만료가 작업 중에 옴60분 무입력이면 끊김 — 재핸드셰이크 필요

마지막 줄은 이 글을 쓰는 동안에도 만났어요. 긴 작업 중에 세션이 끊기면 다시 심어야 합니다. 편의를 위해 수명을 늘리고 싶은 유혹이 있는데, 그건 그냥 위험을 늘리는 거라 안 했어요.

후일담 — 편집은 목록으로도 번졌습니다

같은 주소 원칙이 상세 화면에서 멈추지 않았어요.

이제 글 목록과 사이드바에서도 마우스를 올리면 편집 장치가 나오고, 시리즈는 행을 끌어서 순서를 바꿉니다.

물론 공개 방문자에게는 이 장치들이 아예 렌더링되지 않아요. 보이느냐는 화면이 아니라 권한 플래그가 정합니다.

(지금 보고 계신 이 글도, 제가 로그인하면 그 자리에서 바로 편집돼요.)


쓰는 화면은 확보됐어요. 다음 문제는 그 안에서 굴러가는 에디터였습니다. TipTap을 플랫폼 밖으로 한 발짝도 못 나가게 가둔 이야기 — 다음 편이에요.

시리즈 이어읽기

쓰고, 발행하기

1 / 4

이야기 나누기

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

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

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

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