웹 개발 데일리 브리핑 — 2026-10-23
Soshy··조회수 —

기준 시점: 2026-10-23 06:00 KST
조사 범위: 2026-10-22 06:00 ~ 2026-10-23 06:00 KST
추천 글 범위: 기준 시점 당시 최근 7일
1️⃣ 프론트엔드
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
React, Next.js, Vercel, Vue, Nuxt, Angular, Svelte, SvelteKit, Astro, Remix의 공식 발표와 주요 릴리스 채널을 확인했지만, 이번 24시간에 해당하는 중요한 프레임워크 정식 릴리스나 새로운 렌더링 API 발표는 확인하지 못했다.
Vite, TypeScript, Oxc, ESLint, Biome 등 프론트엔드 개발 도구와 관련해서도 기존 개발 방식에 실질적인 변화를 주는 신규 발표는 확인하지 못했다.
GitLab 19.5에는 MCP 서버의 정식 출시를 포함한 여러 개발 도구 개선이 있었지만, 해당 버전의 실제 출시일은 2026년 10월 15일이다. 따라서 이번 조사 범위의 새로운 릴리스로 포함하지 않았다.
이번에는 새로운 프론트엔드 프레임워크 발표보다 실제 React 애플리케이션을 개발하면서 발견한 문제와 해결 방법을 다룬 기술 글이 눈에 띄었다.
특히 엄격한 Content Security Policy를 유지하면서 CodeMirror를 통합한 사례, 컴포넌트별 타이머를 이용해 불필요한 렌더링을 줄인 사례, 디자인 시스템의 사용 규칙을 TypeScript와 CSS로 강제한 사례를 마지막 추천 글 섹션에서 자세히 다룬다.
2️⃣ 웹 플랫폼·브라우저
이번 조사 범위에서 포함할 만한 주요 신규 발표를 확인하지 못했습니다.
W3C, WHATWG, TC39, MDN, Chrome Developers·Chromium, Mozilla·Firefox, WebKit·Safari와 JavaScript·CSS, 웹 성능 및 접근성 관련 공식 채널을 확인했지만, 이번 기간에 공개된 주요 웹 표준의 정식 채택이나 새로운 브라우저 API의 안정화 발표는 확인하지 못했다.
WebKit은 10월 20~21일 Contributors Meeting을 진행했다. 그러나 행사에서 특정 기술을 논의했다는 사실만으로 해당 기능이 정식 브라우저에 배포됐다고 판단할 수는 없다.
또한 개발 중인 WebMCP와 여러 CSS 기능의 규격 문서는 계속 변경될 수 있지만, 문서의 수정 날짜가 곧 새로운 표준의 정식 출시일을 의미하지는 않는다.
전날 다룬 Cloudflare와 Commerce.com의 WebMCP 통합 역시 이번 브리핑에서 반복하지 않았다.
이번 조사에서는 새로운 웹 플랫폼 기능을 소개하는 대신, 이미 제공되는 브라우저 API를 실제 서비스에 적용할 때 발생하는 보안 및 실행상의 문제를 다룬 글을 추천 섹션에 포함했다.
3️⃣ 백엔드·인프라
Cloudflare Agents SDK 0.30.0, Pi 1.4 지원 — 에이전트 작업의 절전·오류 복구·재시도 구조 개선
- 발표일: 2026-10-22 — 정확한 게시 시각 미확인
- 버전: Cloudflare Agents SDK 0.30.0, Pi 1.4
- 적용 대상: Cloudflare Agents SDK의
PiHarness, Durable Objects 기반 에이전트 - 변경 유형: 기능 개선 및 호환성이 깨지는 저장소 스키마 변경
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-22-pi-harness-pi-1-4/
- Pi 공식 변경 기록: https://github.com/earendil-works/pi/blob/v1.4.0/packages/durable/CHANGELOG.md
Cloudflare가 Agents SDK 0.30.0에서 Pi 1.4를 지원하고, 장시간 실행되는 AI 에이전트의 장애 복구 구조를 개선했다.
이번 변경은 새로운 AI 모델을 추가하는 업데이트가 아니다.
AI 에이전트가 실제 작업을 수행하는 동안 발생하는 실행 중단, 오류, 재시도와 상태 복구를 더 안정적으로 처리하도록 만드는 업데이트다.
일반적인 AI 챗봇에서는 사용자가 질문하면 모델이 응답을 생성하고 요청이 종료된다.
반면 AI 코딩 에이전트는 하나의 요청을 처리하는 동안 여러 작업을 수행한다.
예를 들어 다음과 같은 과정을 생각할 수 있다.
- GitHub Repository에서 코드를 가져온다.
- 프로젝트 구조를 분석한다.
- 필요한 파일을 수정한다.
- 테스트를 실행한다.
- 오류가 발생하면 코드를 다시 수정한다.
- 테스트에 성공하면 PR을 생성한다.
- CI 결과를 확인한다.
이러한 작업은 몇 초 만에 종료되지 않을 수 있다.
외부 API가 일시적으로 실패하거나, 모델 제공업체의 사용량 제한에 걸리거나, 실행 중이던 프로세스가 종료될 수도 있다.
이때 가장 중요한 것은 작업이 중단됐다는 사실보다 어디까지 진행했는지 정확하게 기억하고 이어서 실행할 수 있는가다.
이번 Pi 1.4 지원은 이러한 문제와 관련된다.
PiHarness와 Durable Objects
Cloudflare Agents SDK는 Durable Objects를 이용해 상태를 유지하는 에이전트를 구현할 수 있도록 한다.
Durable Object는 특정 ID와 연결된 실행 단위다.
각 객체는 자신의 상태를 관리하고 지속 저장소를 사용할 수 있다.
일반적인 서버리스 함수와 달리 동일한 논리적 객체에 여러 요청을 연결할 수 있다는 점이 특징이다.
하지만 Durable Object도 항상 메모리에 남아 있는 것은 아니다.
실행 환경이 일시적으로 중단되거나 Hibernation 상태에 들어갈 수 있다.
Hibernation은 현재 실행 중인 객체가 메모리를 계속 점유하지 않도록 쉬는 상태로 전환되는 방식이다.
PiHarness는 이러한 Durable Object 실행 환경과 Pi의 작업 관리 기능을 연결한다.
이번 업데이트에서는 Pi 1.4의 새로운 onWake Callback을 PiHarness에 전달하도록 변경했다.
작업이 잠자는 동안 Durable Object도 Hibernation
예를 들어 AI 모델 제공업체가 요청을 일시적으로 거부했다고 가정해 보자.
에이전트는 일정 시간 기다린 뒤 다시 요청을 시도할 수 있다.
이러한 대기 방식을 Backoff라고 한다.
Backoff는 오류가 발생했을 때 즉시 같은 요청을 반복하지 않고 일정한 간격을 두고 재시도하는 방식이다.
기존에는 에이전트가 재시도를 기다리는 동안 실행 환경이 계속 유지될 수 있었다.
하지만 실제로는 그 시간 동안 수행할 작업이 없다.
이번 변경에서는 Pi의 작업이 Sleep 상태에 들어가면 Durable Object도 Hibernation 상태로 전환될 수 있다.
이후 작업을 다시 실행할 시점이 되면 onWake를 통해 실행을 이어간다.
이러한 구조는 불필요한 메모리 점유와 실행 환경 유지 시간을 줄이는 데 도움이 될 수 있다.
다만 Hibernation을 지원한다고 해서 모든 AI 에이전트의 실행 비용이 일정한 비율로 감소한다는 의미는 아니다.
실제 효과는 에이전트가 얼마나 자주 대기 상태에 들어가는지, 대기 시간이 얼마나 긴지, 실행 환경의 과금 방식이 무엇인지에 따라 달라진다.
이번 발표에는 이러한 비용 절감 효과를 정량적으로 비교한 Benchmark가 포함되지 않았다.
반복적으로 충돌하는 작업을 Parked 상태로 전환
두 번째 변경은 Parked Task다.
AI 에이전트가 어떤 작업을 수행할 때마다 동일한 오류로 Durable Object가 충돌한다고 가정해 보자.
기존 구현에서 단순히 실패한 작업을 계속 재시도하면 문제가 발생할 수 있다.
작업을 시작한다.
같은 오류가 발생한다.
객체가 다시 시작된다.
같은 작업을 다시 실행한다.
다시 충돌한다.
이 과정이 반복되면 에이전트는 실제로 새로운 작업을 수행하지 못한 채 같은 오류만 반복할 수 있다.
이를 Crash Loop라고 한다.
Pi 1.4에서는 반복적으로 객체를 충돌시키는 작업을 무한히 다시 실행하지 않는다.
대신 해당 작업을 Parked 상태로 전환한다.
Parked는 작업을 완전히 삭제한 것이 아니라 추가적인 자동 재시도를 중단하고 별도의 조치를 기다리는 상태다.
개발자는 pending()으로 대기 중인 작업을 조회할 때 다음 상태를 확인할 수 있다.
parked
이후 오류의 원인을 수정하거나 다시 실행할 준비가 됐다면 다음 메서드를 호출할 수 있다.
session.retry()
이렇게 하면 Parked 상태의 작업을 다시 실행할 수 있다.
이 구조는 장애 상황에서 자동 복구와 무한 재시도 사이의 균형을 맞추기 위한 것이다.
모든 오류를 무조건 다시 시도하는 대신, 반복적인 실패가 확인되면 작업을 안전하게 중단한다.
다만 작업이 Parked 상태로 전환됐다고 해서 오류가 자동으로 수정되는 것은 아니다.
개발자는 실제 오류 원인과 재시도 가능 여부를 확인해야 한다.
저장소 오류 이후 실행 환경 복구
세 번째 변경은 Durable Object Storage 오류에 대한 복구다.
AI 에이전트가 여러 단계에 걸쳐 작업을 수행한다면 현재 상태를 지속 저장소에 기록해야 한다.
예를 들어 다음 정보가 저장될 수 있다.
- 현재 실행 중인 작업
- 완료된 모델 호출
- 작업의 결과
- 다음에 실행할 단계
- 재시도해야 하는 요청
하지만 저장소에 접근하는 과정에서도 오류가 발생할 수 있다.
기존에는 저장소 오류가 발생한 이후 PiHarness가 정상적으로 복구되지 못하고, Durable Object가 완전히 재시작될 때까지 이후의 호출이 계속 실패할 수 있었다.
이번 변경에서는 저장소 오류가 발생하면 Pi를 다시 열도록 개선했다.
또한 Wake-up 작업이 실패한 경우에는 작업을 그대로 포기하지 않고 Backoff를 적용해 다시 시도한다.
이는 저장소 오류가 발생했을 때 전체 실행 환경이 장시간 사용할 수 없는 상태로 남는 문제를 줄이기 위한 것이다.
다만 외부 저장소가 지속적으로 사용할 수 없는 상황에서는 재시도만으로 문제가 해결되지 않는다.
이번 기능은 일시적인 오류에 대한 복원력을 높이는 변경이다.
중단된 모델 스트림의 재시도
AI 모델은 응답을 한 번에 반환하지 않고 스트리밍 방식으로 생성할 수 있다.
스트리밍에서는 모델이 생성한 텍스트나 도구 호출 정보를 조금씩 전달한다.
하지만 연결이 중간에 끊어질 수 있다.
예를 들어 네트워크 연결이 종료되거나 모델 제공업체가 일정 시간 동안 새로운 데이터를 전달하지 않는 상황이다.
이번 업데이트에서는 agents/models/pi-ai가 이러한 상황을 일시적인 오류(Transient Error)로 구분할 수 있도록 변경했다.
대표적인 상황은 다음과 같다.
- 모델 스트림이 중간에 끊어짐
- 일정 시간 동안 스트림에 새로운 데이터가 도착하지 않음
Pi는 이러한 오류를 재시도 가능한 상황으로 처리할 수 있다.
따라서 스트림이 중단됐다는 이유만으로 전체 작업이 즉시 실패 상태로 확정되지 않는다.
하지만 중단된 스트림의 재시도 역시 주의가 필요하다.
예를 들어 모델이 이미 외부 시스템을 변경하는 도구를 호출했다면 동일한 작업이 다시 실행되지 않도록 설계해야 한다.
모델 스트림의 재시도 기능과 외부 도구의 중복 실행 방지는 별개의 문제다.
Pi 1.4의 저장소 스키마 변경
이번 업데이트에서 가장 중요한 제한사항은 SQLite 저장소 스키마가 변경된다는 점이다.
Pi 1.4는 Durable Object의 SQLite 데이터베이스에 저장된 세션 정보를 새로운 스키마로 마이그레이션한다.
문제는 기존 Pi 1.3이 새로운 스키마를 읽을 수 없다는 것이다.
따라서 Pi 1.4로 마이그레이션한 이후에는 단순히 이전 코드 버전을 다시 배포한다고 해서 정상적으로 Rollback할 수 없다.
Cloudflare는 이를 명확한 Breaking Change로 안내했다.
이러한 변경은 일반적인 애플리케이션 코드의 호환성 문제보다 복잡하다.
실행 코드는 이전 버전으로 되돌릴 수 있더라도 저장된 데이터는 이미 새로운 형식으로 변경됐기 때문이다.
따라서 실제 운영 환경에서 업데이트할 경우 다음 두 가지를 구분해야 한다.
첫 번째는 Agents SDK의 코드 버전이다.
두 번째는 Durable Object Storage에 저장된 세션 데이터의 스키마 버전이다.
코드만 이전 버전으로 되돌리는 방식은 충분하지 않을 수 있다.
또한 기존 세션을 유지해야 한다면 마이그레이션 이후 어떤 버전에서 데이터를 읽을 수 있는지 확인해야 한다.
Cloudflare는 Agents SDK 0.30.0의 변경 기록과 Pi Durable 1.4.0의 변경 기록을 함께 확인하도록 안내한다.
실제 적용 범위
이번 업데이트는 모든 Cloudflare Workers 애플리케이션에 자동으로 적용되는 변경이 아니다.
PiHarness와 관련된 Agents SDK를 사용하는 프로젝트가 주요 대상이다.
특히 Durable Objects를 이용해 장시간 실행되는 AI 에이전트를 운영하는 경우 영향을 받을 수 있다.
반면 일반적인 HTTP API만 처리하는 Worker는 이번 PiHarness 변경을 직접 사용할 필요가 없다.
이번 발표는 AI 에이전트가 오래 실행되는 동안 발생하는 대기, 저장소 장애, 반복 충돌, 모델 스트림 중단을 각각 다른 상태로 처리하도록 실행 구조를 개선한 업데이트다.
4️⃣ 개발 도구 및 보안
Cloudflare Tunnel, URL 정규화 지원 — Access 인증이 필요한 경로의 규칙 우회 위험 완화
- 발표일: 2026-10-22 — 정확한 게시 시각 미확인
- 지원 버전:
cloudflared2026.10.0 이상 - 적용 대상: Cloudflare Tunnel의 Ingress Rule
- 현재 기본값: 비활성화
- 기본 활성화 예정일: 2027-01-25
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflared-path-normalization/
Cloudflare가 Tunnel의 Ingress Rule을 평가하기 전에 URL 경로를 정규화하는 기능을 추가했다.
이번 변경은 주로 Cloudflare Access 인증과 연결된 경로 기반 라우팅 규칙의 보안 문제를 줄이기 위한 것이다.
Cloudflare Tunnel은 외부 인터넷에 직접 노출하지 않은 서버를 Cloudflare 네트워크와 연결하는 기능이다.
예를 들어 회사 내부의 관리자 페이지를 외부에서 접근할 수 있도록 구성하되, 실제 서버의 공개 IP 주소나 직접적인 인바운드 연결을 제공하지 않을 수 있다.
이때 요청을 어떤 내부 서비스로 전달할지 결정하는 설정이 Ingress Rule이다.
Ingress Rule의 역할
하나의 Tunnel에서 여러 서비스를 제공한다고 가정해 보자.
예를 들어 다음과 같은 구성이다.
| 요청 경로 | 전달할 서비스 |
|---|---|
/api/* | API 서버 |
/admin/* | 관리자 서버 |
| 그 외 | 일반 웹 애플리케이션 |
Ingress Rule은 요청 URL과 설정된 조건을 비교해 어떤 서비스로 요청을 전달할지 결정한다.
이러한 구조는 편리하지만, 요청 경로를 서로 다른 시스템이 다르게 해석하면 문제가 발생할 수 있다.
특히 인증이 필요한 경로에서는 중요하다.
예를 들어 /admin/*에 접근하려면 Cloudflare Access 인증을 통과해야 한다고 가정해 보자.
Ingress Rule이 요청을 일반 웹 애플리케이션으로 판단했지만 Origin 서버는 같은 경로를 관리자 페이지로 해석한다면, 의도한 인증 정책이 적용되지 않을 가능성이 있다.
문제의 핵심은 Cloudflare Tunnel이 검사하는 경로와 실제 Origin 서버가 해석하는 경로가 일치하지 않을 수 있다는 점이다.
URL 정규화가 필요한 이유
URL에는 여러 표현 방식이 존재한다.
예를 들어 경로에 중복된 구분자나 인코딩된 문자가 포함될 수 있다.
서로 다른 서버와 프록시가 이러한 문자를 해석하는 방식은 완전히 동일하지 않을 수 있다.
어떤 시스템은 요청 경로를 정규화한 뒤 비교한다.
다른 시스템은 전달받은 문자열을 그대로 비교할 수 있다.
이러한 차이가 존재하면 실제로는 같은 리소스를 가리키는 요청도 서로 다른 라우팅 규칙에 일치할 수 있다.
Cloudflare의 새로운 기능은 Ingress Rule을 평가하기 전에 요청 경로를 정규화한다.
따라서 경로를 표현하는 방식이 다르더라도 보다 일관된 규칙으로 판단할 수 있다.
특히 Cloudflare Access 인증을 요구하는 Ingress Rule이 올바르게 적용되도록 하는 것이 목적이다.
Origin에는 원래 URL 경로 전달
이번 변경에서 중요한 점은 Origin 서버로 전달되는 URL 경로는 변경하지 않는다는 사실이다.
Cloudflare는 경로를 정규화해 Ingress Rule을 평가한다.
하지만 실제 Origin으로 전달하는 URL에는 원래 경로를 유지한다.
따라서 이번 기능은 Origin 서버가 받는 모든 URL을 정규화된 문자열로 강제로 바꾸는 기능이 아니다.
구분하면 다음과 같다.
| 단계 | 사용되는 경로 |
|---|---|
| Ingress Rule 평가 | 정규화된 URL 경로 |
| Origin 서버로 요청 전달 | 원래 요청의 URL 경로 |
이러한 설계는 보안 정책의 판단을 일관되게 하면서도 Origin 애플리케이션의 URL 처리 동작을 변경하지 않으려는 선택이다.
다만 실제 애플리케이션이 어떤 경로를 어떻게 해석하는지는 Origin의 웹서버와 프레임워크에 따라 달라진다.
따라서 URL 정규화 기능만으로 모든 Path Traversal이나 URL 해석 불일치 문제가 해결된다고 볼 수는 없다.
현재는 기본적으로 비활성화
Cloudflare는 이번 기능을 즉시 모든 Tunnel에 강제 적용하지 않았다.
cloudflared 2026.10.0부터 기능을 사용할 수 있지만 현재 기본값은 비활성화다.
운영자는 명시적으로 설정해야 한다.
환경 변수로 활성화하려면 다음 값을 사용한다.
TUNNEL_DISABLE_PATH_NORMALIZATION=false
이름이 다소 혼동될 수 있다.
환경 변수 이름에 DISABLE이 포함돼 있기 때문이다.
해당 값을 false로 설정하면 정규화 기능을 비활성화하지 않는다는 의미이므로 실제 정규화가 활성화된다.
환경 변수를 변경한 이후에는 cloudflared를 다시 시작해야 한다.
복제본이 여러 개라면 각 복제본에 동일한 설정을 적용해야 한다.
일부 복제본에만 적용하면 요청이 어떤 복제본을 통과하는지에 따라 라우팅 결과가 달라질 수 있기 때문이다.
2027년 1월 25일 기본 활성화 예정
Cloudflare는 2027년 1월 25일부터 공개하는 새로운 cloudflared 릴리스에서 URL 정규화를 기본적으로 활성화할 예정이라고 발표했다.
이는 2027년 1월 25일에 기존의 모든 Tunnel이 별도 조건 없이 동시에 변경된다는 의미는 아니다.
해당 날짜부터 새로운 릴리스의 기본 동작이 변경된다는 계획이다.
따라서 현재 특정 버전을 고정해 운영하고 있다면 실제로 업데이트하는 시점에 영향을 받을 수 있다.
또한 경로 기반 라우팅 정책을 사용하고 있다면 정규화를 활성화했을 때 어떤 Ingress Rule이 선택되는지 확인해야 한다.
정규화 이전과 이후에 서로 다른 규칙이 일치할 수 있기 때문이다.
Cloudflare는 Access 인증을 요구하는 규칙이 없는 구성에서는 영향을 받지 않는다고 설명한다.
이번 발표는 URL 경로를 해석하는 방식의 차이가 인증 정책 우회로 이어질 수 있다는 문제를 줄이면서, 향후 기본 동작 변경에 대비할 수 있도록 사전 활성화 옵션을 제공한 보안 업데이트다.
Cloudflare MCP 서버, 기존 /sse 엔드포인트 종료 — /mcp로 이전하지 않으면 HTTP 410 반환
- 발표일: 2026-10-22 — 정확한 게시 시각 미확인
- 적용 대상: Cloudflare의 제품별 MCP 서버
- 변경 유형: 기존 연결 엔드포인트 종료
- 새 엔드포인트:
/mcp - 원문: https://developers.cloudflare.com/changelog/post/2026-10-22-cloudflare-mcp-servers-retire-sse/
Cloudflare가 제품별 MCP 서버에서 기존 /sse 엔드포인트를 더 이상 제공하지 않고 /mcp 엔드포인트만 지원하도록 변경했다.
이번 변경은 AI 에이전트나 코딩 도구에 Cloudflare의 MCP 서버를 연결해 사용하는 프로젝트에 영향을 줄 수 있다.
MCP(Model Context Protocol)는 AI 모델이나 에이전트가 외부 도구와 데이터에 접근할 수 있도록 하는 프로토콜이다.
예를 들어 AI 코딩 에이전트가 Cloudflare 공식 문서를 검색하거나 특정 서비스의 설정 정보를 조회할 수 있다.
이때 MCP 클라이언트는 외부 MCP 서버의 주소를 설정해야 한다.
기존에는 일부 MCP 서버에서 SSE 기반 연결 경로를 사용했다.
대표적인 형태는 다음과 같다.
/sse
하지만 이번 업데이트 이후 Cloudflare의 제품별 MCP 서버는 다음 경로만 제공한다.
/mcp
기존 SSE 방식과 새로운 연결 방식
SSE(Server-Sent Events)는 서버에서 클라이언트로 이벤트를 지속적으로 전달하는 기술이다.
초기의 원격 MCP 연결에서는 SSE를 이용하는 방식이 널리 사용됐다.
그러나 원격 MCP 연결의 전송 방식은 이후 발전해 왔다.
현재는 Streamable HTTP 기반의 MCP 연결을 사용할 수 있다.
이번 Cloudflare 변경은 이러한 새로운 연결 방식으로 이전하는 과정과 관련된다.
다만 여기서 구분해야 할 부분이 있다.
Cloudflare가 /sse 경로를 종료했다는 사실이 웹 표준인 SSE 자체가 폐지됐다는 의미는 아니다.
SSE는 여전히 다양한 웹 애플리케이션에서 사용할 수 있는 기술이다.
또한 다른 기업의 모든 MCP 서버가 /sse 경로를 종료했다는 의미도 아니다.
이번 변경의 대상은 Cloudflare가 제공하는 제품별 MCP 서버다.
기존 연결에 HTTP 410 반환
기존 /sse 주소로 요청하면 서버는 다음 상태 코드를 반환한다.
410 Gone
HTTP 410은 요청한 리소스가 더 이상 제공되지 않는다는 의미다.
Cloudflare는 이 응답에 새로운 /mcp 주소를 포함한다.
따라서 일부 MCP 클라이언트에서는 연결 오류 메시지와 함께 이전해야 할 주소를 확인할 수 있다.
예를 들어 Cloudflare 문서 MCP 서버의 새로운 주소는 다음과 같다.
https://docs.mcp.cloudflare.com/mcp
기존 주소가 다음과 같았다면
https://docs.mcp.cloudflare.com/sse
MCP 클라이언트의 설정을 새로운 /mcp 주소로 변경해야 한다.
여기서 중요한 점은 클라이언트가 새로운 주소를 자동으로 찾아 연결한다고 가정해서는 안 된다는 것이다.
사용하는 MCP 클라이언트에 따라 직접 설정을 변경해야 할 수 있다.
재인증 필요
Cloudflare는 기존 /sse 경로에서 로그인했던 사용자에게 재인증이 필요할 수 있다고 안내했다.
예를 들어 MCP 클라이언트가 Cloudflare 계정과 OAuth 연결을 생성한 상태였다고 가정해 보자.
이후 MCP 서버 주소를 새로운 /mcp 경로로 변경하면 클라이언트가 다시 로그인하도록 요청할 수 있다.
이는 기존 연결에 사용했던 인증 상태를 새로운 연결 경로에서 그대로 재사용하지 못할 수 있기 때문이다.
따라서 단순히 URL 문자열만 변경하는 것으로 모든 연결 복구가 완료되지 않을 수 있다.
반면 이미 /mcp 주소를 사용하고 있는 연결은 이번 변경의 영향을 받지 않는다.
코딩 에이전트에서 발생할 수 있는 문제
MCP 서버를 코딩 에이전트에 연결해 사용하는 경우 이번 변경은 개발 환경에 직접적인 영향을 줄 수 있다.
예를 들어 에이전트가 Cloudflare 문서를 검색하는 도구를 사용한다고 가정해 보자.
기존 MCP 설정에 /sse 주소가 남아 있다면 서버 연결 단계에서 HTTP 410 오류가 발생할 수 있다.
이 경우 에이전트는 실제 문서를 검색하기 전에 실패한다.
개발자는 오류의 원인을 모델의 도구 호출 문제나 인증 오류로 오해할 수 있다.
하지만 이번 변경에서는 엔드포인트 자체가 종료된 것이므로 먼저 MCP 서버 주소를 확인해야 한다.
또한 여러 개발 도구에서 동일한 MCP 서버를 사용하고 있다면 각 도구의 연결 설정을 확인해야 할 수 있다.
이번 업데이트는 새로운 MCP 도구나 AI 모델을 추가한 것이 아니다.
오래된 원격 MCP 연결 경로를 종료하고 새로운 /mcp 엔드포인트로 통합한 호환성 변경이다.
Cloudflare Tunnel, 특정 Tunnel에만 로그 조회 권한 부여하는 역할 정식 출시 — 설정 변경 권한과 분리
- 발표일: 2026-10-22 — 정확한 게시 시각 미확인
- 안정화 단계: Generally Available
- 적용 대상: Cloudflare Tunnel, Cloudflare One
- 신규 역할: Cloudflare Tunnel - Granular Connector cloudflared Read And Monitor
- 원문: https://developers.cloudflare.com/changelog/post/2026-10-22-granular-read-and-monitor-ga/
Cloudflare가 특정 Tunnel의 설정을 조회하고 실시간 로그를 확인할 수 있지만 설정을 변경할 수는 없는 새로운 접근 권한 역할을 정식 출시했다.
클라우드 인프라를 운영할 때는 여러 사람이 동일한 시스템을 관리할 수 있다.
예를 들어 다음과 같은 역할이 존재할 수 있다.
- 네트워크 설정을 변경하는 관리자
- 장애를 조사하는 개발자
- 로그를 확인하는 고객지원 담당자
- 보안 정책을 점검하는 운영자
이러한 사용자들이 모두 같은 권한을 가져야 하는 것은 아니다.
예를 들어 고객지원 담당자는 Tunnel이 정상적으로 연결돼 있는지 확인해야 할 수 있다.
하지만 해당 Tunnel의 라우팅 설정을 변경하거나 삭제할 권한까지 필요하지는 않을 수 있다.
이번 업데이트는 이러한 상황에서 조회 및 모니터링 권한과 설정 변경 권한을 분리하도록 한다.
기존 Read 역할과의 차이
Cloudflare에는 기존에도 Tunnel에 대한 읽기 전용 역할이 존재했다.
이 역할을 사용하면 지정된 Tunnel의 설정 정보를 확인할 수 있었다.
하지만 기존 Read 역할에는 Remote Log Streaming 권한이 포함되지 않았다.
따라서 로그를 확인하려는 사용자에게는 별도의 권한이 필요할 수 있었다.
이번에 추가된 역할은 기존 읽기 권한에 실시간 로그 조회를 추가한다.
| 기능 | 기존 Granular Read | 신규 Read And Monitor |
|---|---|---|
| 지정된 Tunnel 조회 | 가능 | 가능 |
| IP 및 Hostname Route 조회 | 가능 | 가능 |
| Connector Host 정보 조회 | 가능 | 가능 |
| Remote Log Streaming | 미지원 | 지원 |
| Tunnel 설정 변경 | 불가 | 불가 |
| 계정의 모든 Tunnel 자동 조회 | 불가 | 불가 |
이러한 구조에서는 장애 조사에 필요한 정보를 제공하면서 실제 설정을 변경할 수 있는 권한은 부여하지 않는다.
리소스 단위 권한
새로운 역할의 중요한 특징은 계정 전체가 아니라 특정 Tunnel에만 권한을 부여할 수 있다는 점이다.
예를 들어 하나의 Cloudflare 계정에 여러 고객의 Tunnel이 존재한다고 가정해 보자.
고객 A의 문제를 조사하는 담당자에게 고객 B와 C의 Tunnel까지 조회할 수 있는 권한을 제공할 필요는 없다.
관리자는 고객 A의 Tunnel에만 Read And Monitor 역할을 부여할 수 있다.
이러한 방식은 최소 권한 원칙(Principle of Least Privilege)에 해당한다.
최소 권한 원칙은 사용자가 자신의 업무를 수행하는 데 필요한 범위의 권한만 가지도록 하는 보안 설계다.
권한 범위를 좁히면 계정이 탈취되거나 실수로 잘못된 작업을 수행했을 때 영향을 줄일 수 있다.
Remote Log Streaming의 역할
Remote Log Streaming은 Tunnel의 실행 로그를 원격에서 확인하는 기능이다.
예를 들어 다음과 같은 상황을 조사할 수 있다.
- Tunnel 연결이 반복적으로 종료됨
- Origin 서버에 연결할 수 없음
- 특정 Route로 요청이 전달되지 않음
- Connector 실행 과정에서 오류 발생
운영자는 해당 Tunnel이 어떤 상태인지 확인하고 관련 로그를 분석할 수 있다.
하지만 로그를 읽을 수 있다는 사실이 원인을 자동으로 해결하는 기능을 제공한다는 의미는 아니다.
실제 설정을 수정하거나 Tunnel을 재시작해야 한다면 적절한 변경 권한을 가진 관리자가 작업해야 한다.
이번 역할은 장애 조사와 설정 변경이라는 두 종류의 책임을 분리하는 데 도움이 된다.
적용 범위
이번 역할은 Generally Available 상태다.
Cloudflare의 Resource-scoped Role 기능을 통해 부여할 수 있다.
그러나 기존 Read 역할을 사용하는 모든 사용자에게 로그 조회 권한이 자동으로 추가되는 것은 아니다.
필요한 경우 새로운 역할을 명시적으로 부여해야 한다.
또한 이번 역할은 Tunnel을 관리하는 사용자의 접근 권한에 관한 것이다.
Tunnel을 통해 제공하는 실제 웹 애플리케이션의 사용자 인증 정책을 변경하는 기능은 아니다.
이번 발표는 인프라 운영 과정에서 로그 조회를 위한 권한과 실제 설정을 변경하는 권한을 분리해 보다 세밀한 접근 제어를 제공한 업데이트다.
📚 추천 글
Building a Live UI Schema Playground for OpenEverest (LFX Mentorship 2026)
- 저자: Naman Raj
- 발행 매체: DEV Community
- 게시일: 2026-10-22
- 원문: https://dev.to/denyme24/building-a-live-ui-schema-playground-for-openeverest-lfx-mentorship-2026-58m6
OpenEverest 프로젝트에서 YAML 기반 UI Schema를 실시간으로 편집하고, 실제 React 컴포넌트로 렌더링되는 결과를 즉시 확인하는 개발 도구를 구현한 과정을 설명한 글이다.
이번 글의 특징은 완성된 기능을 소개하는 데 그치지 않는다는 점이다.
개발자가 처음 선택한 아키텍처가 운영 환경의 보안 정책과 충돌한 이유, 이를 해결하기 위해 설계했던 여러 구성요소를 왜 최종적으로 삭제했는지까지 설명한다.
특히 React의 상태 관리와 CodeMirror, Zod, Content Security Policy를 함께 사용하는 실제 프로젝트에서 발생한 문제를 구체적으로 다룬다.
OpenEverest의 UI Schema 구조
OpenEverest는 Kubernetes 환경에서 데이터베이스를 운영하기 위한 오픈소스 플랫폼이다.
데이터베이스 종류에 따라 필요한 설정이 다르기 때문에 Provider라는 플러그인 구조를 사용한다.
Provider는 Kubernetes의 Custom Resource Definition(CRD)을 통해 등록된다.
CRD는 Kubernetes가 기존에 알지 못했던 새로운 리소스 유형을 정의할 수 있도록 하는 기능이다.
예를 들어 PostgreSQL과 MongoDB는 데이터베이스를 생성할 때 필요한 설정이 다를 수 있다.
OpenEverest는 이러한 설정을 UI Schema로 표현한다.
UI Schema는 YAML로 작성한다.
YAML에는 다음 정보가 포함될 수 있다.
- 데이터베이스의 배포 형태
- 설정 화면의 섹션
- 입력 필드의 종류
- 필드의 Label
- 기본값
- 유효성 검사 조건
- 실제 API 요청에 사용할 데이터 경로
프론트엔드에서는 UIGenerator 컴포넌트가 해당 Schema를 읽고 실제 입력 양식을 생성한다.
문제는 기존 개발 과정이었다.
기존에는 UI를 확인하려면 Kubernetes에 배포해야 했다
기존 방식에서는 Provider의 UI Schema를 수정한 뒤 실제 Kubernetes 클러스터에 다시 배포해야 했다.
이후 OpenEverest에서 데이터베이스 생성 화면을 열어야 변경된 입력 양식을 확인할 수 있었다.
YAML에 작은 오타가 있거나 필드 설정이 잘못됐다면 다시 수정하고 배포해야 했다.
즉 단순한 UI 변경을 확인하기 위해 비교적 무거운 인프라 작업을 반복해야 했다.
저자가 구현한 새로운 Playground는 이러한 과정을 줄이기 위한 것이다.
개발자는 브라우저에서 YAML을 직접 수정한다.
수정한 Schema가 유효하다면 오른쪽 영역에 실제 입력 양식이 렌더링된다.
오류가 있다면 에디터에 해당 오류가 표시된다.
이러한 방식으로 Schema를 수정하고 결과를 확인하는 과정을 하나의 화면에서 수행할 수 있다.
Playground의 주요 구성
완성된 화면은 /plugin-developer 경로에서 제공된다.
주요 기능은 다음과 같다.
| 기능 | 구현 방식 |
|---|---|
| YAML 편집 | CodeMirror 6 |
| YAML 문법 검사 | yaml 패키지 |
| Schema 구조 검사 | Zod |
| 실제 UI 미리보기 | 기존 UIGenerator 재사용 |
| 입력 양식의 섹션 이동 | Stepper |
| 배포 형태 선택 | Topology Picker |
| Kubernetes Namespace 선택 | Namespace Picker |
| 에디터·미리보기 크기 조절 | Split Pane |
중요한 부분은 별도의 미리보기 렌더러를 새롭게 작성하지 않았다는 사실이다.
실제 데이터베이스 생성 화면에서 사용하는 UIGenerator를 그대로 재사용한다.
이렇게 하면 Playground에서 보이는 화면과 실제 서비스에서 보이는 화면 사이의 차이를 줄일 수 있다.
만약 Playground 전용 렌더러를 별도로 구현했다면 새로운 필드 유형이 추가될 때 두 렌더러를 모두 수정해야 할 수 있다.
저자는 이를 피하기 위해 기존 컴포넌트를 사용하는 방식을 선택했다.
가장 큰 제약: 엄격한 Content Security Policy
프로젝트에는 중요한 보안 제약이 존재했다.
OpenEverest는 데이터베이스와 Credential을 다루기 때문에 엄격한 Content Security Policy(CSP)를 사용한다.
CSP는 웹페이지가 어떤 스크립트와 스타일, 외부 리소스를 로딩하거나 실행할 수 있는지 제한하는 보안 정책이다.
이 프로젝트에서는 다음 조건을 사용했다.
script-src 'self'unsafe-eval금지unsafe-inline금지- 외부 CDN 스크립트 사용 불가
- 동적으로 생성한 Style에는 Nonce 필요
Nonce는 서버가 응답마다 생성하는 임의의 값이다.
브라우저는 해당 값이 지정된 Style이나 Script만 실행하도록 제한할 수 있다.
기존 Prototype은 Monaco Editor를 사용했다.
하지만 Monaco는 프로젝트의 CSP 정책과 충돌했다.
특히 외부 CDN에서 코드를 가져오는 방식과 eval()에 의존하는 실행 구조가 문제였다.
개발자는 이를 해결하기 위해 CSP를 느슨하게 변경할 수도 있었다.
그러나 저자는 보안 정책을 완화하지 않기로 결정했다.
개발 편의성을 위해 기존 보안 정책을 약화시키지 않고, 해당 제약 안에서 동작하는 에디터를 구현하는 것을 목표로 했다.
첫 번째 설계: Sandboxed iframe
저자가 처음 선택한 방법은 CodeMirror를 별도의 iframe 안에서 실행하는 것이었다.
iframe은 웹페이지 안에 다른 문서를 삽입하는 HTML 요소다.
sandbox 속성을 이용하면 iframe이 수행할 수 있는 작업을 제한할 수 있다.
저자는 다음 설정을 사용했다.
sandbox="allow-scripts"
여기서 중요한 점은 allow-same-origin을 추가하지 않았다는 것이다.
이렇게 하면 iframe은 JavaScript를 실행할 수 있지만 부모 페이지와 같은 Origin으로 취급되지 않는다.
브라우저는 해당 iframe을 불투명한 Origin(Opaque Origin)으로 처리한다.
따라서 iframe 안의 코드가 부모 페이지의 쿠키나 Local Storage, 로그인 정보에 직접 접근하는 것을 제한할 수 있다.
부모 React 애플리케이션과 iframe은 직접 객체를 공유할 수 없으므로 postMessage로 통신했다.
Typed Message Protocol
저자는 iframe과 React 애플리케이션 사이의 통신 규칙을 TypeScript로 정의했다.
부모 페이지에서 iframe으로 전달하는 메시지는 다음과 같았다.
initsetThemesetDiagnostics
iframe에서 부모 페이지로 전달하는 메시지는 다음과 같았다.
readychangeerror
이러한 Message Protocol을 이용하면 서로 다른 실행 문맥에 있는 두 프로그램이 어떤 데이터를 교환할지 명확하게 정의할 수 있다.
또한 외부에서 임의의 메시지가 전달될 가능성을 고려해 런타임 타입 검사를 수행했다.
메시지를 보낸 Window가 실제 iframe인지도 확인했다.
이러한 검증이 필요한 이유는 TypeScript 타입이 브라우저의 런타임 보안을 자동으로 보장하지 않기 때문이다.
외부에서 전달된 메시지는 실제 TypeScript 타입 정의와 관계없이 임의의 데이터를 포함할 수 있다.
따라서 postMessage를 사용하는 구조에서는 데이터의 형태와 발신자를 모두 검증해야 한다.
Zod를 이용한 Schema 검증
Playground는 YAML의 유효성을 두 단계로 검사한다.
첫 번째는 문법 검사다.
YAML Parser가 입력된 문자열을 읽을 수 있는지 확인한다.
예를 들어 들여쓰기가 잘못됐거나 문법적으로 올바르지 않은 문자열이 존재한다면 오류를 표시한다.
두 번째는 구조 검사다.
문법적으로 올바른 YAML이라고 해서 OpenEverest가 요구하는 UI Schema도 올바른 것은 아니다.
예를 들어 입력 필드에 필요한 uiType이 존재하지 않을 수 있다.
또는 섹션에 포함돼야 할 필드 목록이 누락됐을 수 있다.
저자는 이러한 문제를 검사하기 위해 Zod를 사용했다.
Zod는 JavaScript 실행 환경에서 실제 데이터 구조를 검사할 수 있는 라이브러리다.
TypeScript 타입은 컴파일 이후 런타임에 남지 않지만 Zod Schema는 실제 입력값을 검증할 수 있다.
또한 UI Schema에서는 그룹 안에 다른 그룹이 포함될 수 있다.
이러한 재귀적인 구조는 z.lazy를 이용해 정의했다.
오류가 발생한 YAML 위치 표시
단순히 Schema가 잘못됐다는 메시지를 표시하는 것만으로는 충분하지 않다.
개발자는 어떤 줄에서 오류가 발생했는지 알아야 한다.
Zod는 오류가 발생한 데이터 경로를 제공한다.
예를 들어 다음과 같은 경로다.
replica.sections.basicInfo.components.nodes
하지만 이 경로는 JavaScript 객체의 구조를 나타낸다.
실제 YAML 텍스트에서 몇 번째 줄에 해당하는지는 직접 알려주지 않는다.
저자는 이를 해결하기 위해 YAML Parser가 생성한 Syntax Tree를 사용했다.
Syntax Tree는 원본 코드의 구조를 트리 형태로 표현한 자료구조다.
각 Node에는 원본 텍스트에서의 위치 정보가 포함될 수 있다.
저자는 Zod의 오류 경로를 따라 YAML Syntax Tree를 탐색했다.
그리고 실제 문서에서 존재하는 가장 깊은 Node를 찾았다.
예를 들어 잘못된 값이 존재한다면 해당 값에 밑줄을 표시한다.
반면 필수 필드 자체가 누락됐다면 해당 필드의 위치가 존재하지 않는다.
이 경우에는 상위 컴포넌트의 Key에 밑줄을 표시한다.
이는 구조화된 데이터의 검증 오류를 실제 텍스트 편집기의 위치 정보와 연결하는 방법이다.
중복된 필드 유형 정의 제거
초기 구현에서는 사용할 수 있는 uiType 목록을 검증 코드에 직접 작성했다.
하지만 이 방식은 유지보수 문제가 있다.
예를 들어 실제 UIGenerator에 새로운 입력 필드 유형이 추가됐다고 가정해 보자.
렌더러에서는 해당 필드를 정상적으로 지원하지만 Playground의 검증 목록은 업데이트되지 않았을 수 있다.
이 경우 실제로 사용할 수 있는 필드가 Playground에서는 오류로 표시된다.
저자는 이를 해결하기 위해 UIGenerator가 사용하는 FieldType Enum을 그대로 가져오도록 변경했다.
따라서 실제 렌더러에 새로운 필드 유형이 추가되면 검증에서도 해당 유형을 사용할 수 있다.
같은 규칙을 여러 곳에서 별도로 정의하지 않고 하나의 원천에서 공유하도록 만든 변경이다.
React의 Derived State
저자는 처음에 YAML 검증 결과를 React State에 저장했다.
그리고 YAML 텍스트가 변경될 때마다 useEffect에서 검증을 다시 실행했다.
문제는 YAML이 변경되는 경로가 여러 개였다는 점이다.
사용자가 직접 입력할 수도 있다.
Format 버튼을 눌러 전체 문자열을 변경할 수도 있다.
이러한 변경 경로마다 검증 함수를 직접 호출하면 일부 상황에서 검증 결과가 최신 텍스트와 일치하지 않을 수 있다.
저자는 이를 해결하기 위해 검증 결과를 별도의 State로 저장하지 않도록 변경했다.
대신 YAML 텍스트에서 검증 결과를 계산하도록 했다.
핵심 구조는 다음과 같다.
useMemo(() => validateSchema(yamlText), [yamlText])
이렇게 하면 YAML 문자열이 변경될 때마다 검증 결과도 새로운 문자열에 맞춰 계산된다.
검증 결과를 별도의 State로 동기화할 필요가 줄어든다.
이는 React에서 자주 등장하는 Derived State 문제와 연결된다.
기존 데이터에서 계산할 수 있는 값을 별도의 State에 저장하면 두 값의 일관성을 직접 관리해야 한다.
반면 계산 결과로 표현하면 이러한 동기화 문제를 줄일 수 있다.
최종 설계에서 iframe 제거
가장 흥미로운 부분은 저자가 처음 구현한 iframe 구조를 최종적으로 삭제했다는 사실이다.
Sandboxed iframe은 이론적으로 합리적인 접근이었다.
하지만 실제 운영 환경의 CSP에서는 기대한 대로 동작하지 않았다.
저자는 다른 방향을 선택했다.
CodeMirror를 iframe에서 분리해 일반적인 React 컴포넌트로 직접 통합했다.
이 과정에서 다음 코드를 제거했다.
- iframe HTML 생성기
postMessage통신 프로토콜- 별도의 CodeMirror Bundle
- Bundle Hash Cache
- 에디터 전용 빌드 단계
처음에는 보안 정책을 지키기 위해 많은 추가 구조가 필요하다고 생각했지만, 최종적으로는 더 단순한 구조가 같은 보안 요구사항을 충족하게 된 것이다.
CodeMirror의 CSP Nonce 지원
CodeMirror 6는 일반적인 사용에서 eval()에 의존하지 않는다.
따라서 script-src 'self'와 unsafe-eval 금지 정책을 유지할 수 있다.
다만 CodeMirror는 실행 중에 Style 요소를 동적으로 생성한다.
OpenEverest의 CSP에서는 이러한 Style에도 Nonce가 필요하다.
CodeMirror는 다음 설정을 제공한다.
EditorView.cspNonce
저자는 서버가 HTML에 포함하는 CSP Nonce를 읽어 CodeMirror에 전달하도록 했다.
그러면 CodeMirror가 생성하는 Style에도 올바른 Nonce가 적용된다.
따라서 unsafe-inline을 활성화할 필요가 없다.
추가로 CodeMirror의 Lint 기능은 작은 SVG 이미지를 data: URL로 표현한다.
이를 위해 img-src 정책에 data:를 추가했다.
하지만 스크립트 실행에 관한 주요 보안 정책은 그대로 유지했다.
이번 구현에서 필요한 CSP 변경은 Lint 표시용 이미지의 data: URL 허용이었다.
이는 모든 동적 스타일을 허용하는 것과 다른 변경이다.
CodeMirror와 React의 상태 충돌 방지
iframe을 제거하면 또 다른 문제가 발생한다.
CodeMirror와 React가 동일한 문서에서 동작하기 때문이다.
예를 들어 사용자가 Format YAML 버튼을 눌렀다고 가정해 보자.
React가 새로운 YAML 문자열을 CodeMirror에 전달한다.
CodeMirror는 자신의 내용이 변경됐다는 이벤트를 발생시킨다.
React는 해당 이벤트를 사용자의 입력으로 판단해 다시 State를 변경할 수 있다.
이러한 구조에서는 동일한 변경이 불필요하게 여러 번 처리될 수 있다.
저자는 CodeMirror의 Annotation을 이용해 변경의 출처를 구분했다.
React가 프로그램으로 적용한 변경에는 별도의 Annotation을 부여한다.
이후 CodeMirror의 Change Listener는 해당 Annotation이 있는 변경을 사용자 입력으로 다시 전달하지 않는다.
또한 150ms Debounce를 사용했다.
사용자가 타이핑할 때마다 검증을 실행하는 대신 마지막 입력 이후 150ms 동안 추가 입력이 없으면 React에 변경을 전달한다.
이는 에디터의 반응성을 유지하면서 불필요한 검증을 줄이기 위한 선택이다.
실제 테스트와 공통 컴포넌트 오류
저자는 Playground의 주요 로직을 세 개의 Custom Hook으로 분리했다.
useSchemaValidationusePreviewNamespaceuseSplitPane
각 Hook은 서로 다른 책임을 가진다.
검증 로직, Namespace 선택, 화면 영역 크기 조절을 각각 독립적으로 관리한다.
Split Pane은 사용자가 에디터와 미리보기의 너비를 변경할 수 있도록 한다.
다만 각 영역이 완전히 사라지지 않도록 너비를 20~80% 범위로 제한했다.
또한 테스트 과정에서 기존 공통 Hook인 useTopology의 문제도 발견했다.
사용자가 YAML을 수정해 기존 Topology를 제거하면 선택된 Topology가 더 이상 존재하지 않을 수 있었다.
하지만 Hook은 오래된 선택값을 계속 유지했다.
저자는 현재 선택한 Topology가 실제 목록에 존재하는지 확인하고, 존재하지 않으면 기본 Topology로 돌아가도록 수정했다.
이는 Playground에서 발견한 문제가 실제 데이터베이스 생성 화면의 공통 로직에도 영향을 줄 수 있다는 사례다.
이번 글은 보안 정책을 약화시키지 않으면서 에디터를 통합하고, React의 Derived State와 실행 문맥 사이의 통신 문제를 해결하며, 필요하지 않은 추상화를 제거한 실제 개발 기록이다.
특히 처음 설계한 구조를 유지하는 것보다 운영 환경에서 검증된 더 단순한 구조로 변경한 이유를 자세히 설명한다.
Three clocks in one window at 1s, 5s and 30s, and not one component uses what the hook returns
- 저자: Daniel Pertu
- 발행 매체: DEV Community
- 게시일: 2026-10-22
- 원문: https://dev.to/daniel_pertu/three-clocks-in-one-window-at-1s-5s-and-30s-and-not-one-component-uses-what-the-hook-returns-1p74
React와 Electron으로 개발한 데스크톱 애플리케이션에서 컴포넌트마다 서로 다른 주기의 타이머를 사용해 상대 시간을 갱신하면서 불필요한 렌더링을 줄인 방법을 설명한 글이다.
이번 글의 핵심은 코드가 복잡하지 않다는 점이다.
저자가 구현한 Custom Hook은 약 12줄이다.
하지만 그 Hook을 어떤 컴포넌트에서 얼마나 자주 실행할지 결정하는 과정에는 여러 사용자 경험과 성능 관련 판단이 포함돼 있다.
특히 흥미로운 점은 세 컴포넌트 모두 Hook이 반환하는 값을 사용하지 않는다는 사실이다.
상대 시간 표시의 문제
저자가 개발한 Notifio는 백그라운드에서 여러 검색 작업을 실행하고 결과를 보여주는 데스크톱 애플리케이션이다.
화면에는 다음과 같은 정보가 나타난다.
- 마지막 검색 시점
- 다음 재시도까지 남은 시간
- 검색 작업의 현재 상태
- 최근 발견한 결과의 생성 시점
이러한 정보는 현재 시간에 따라 계속 달라진다.
예를 들어 다음 문장을 생각해 보자.
마지막 확인: 12초 전
화면에 이 문장이 표시된 시점에는 정확할 수 있다.
하지만 10초가 지나도 그대로 표시된다면 실제로는 22초 전이라는 사실을 나타내지 못한다.
따라서 상대 시간을 표시하는 UI는 일정한 간격으로 다시 계산해야 한다.
가장 단순한 방법은 전체 애플리케이션에서 하나의 타이머를 실행하는 것이다.
예를 들어 매초 현재 시간을 갱신하고 모든 컴포넌트에 전달할 수 있다.
하지만 이 방법에는 문제가 있다.
모든 컴포넌트가 1초 단위의 정확도를 필요로 하지는 않기 때문이다.
useNow Hook
저자는 다음 형태의 Custom Hook을 사용했다.
useNow(intervalMs = 1000, active = true)
이 Hook은 내부적으로 현재 시간을 State에 저장한다.
그리고 지정한 간격으로 State를 갱신한다.
State가 변경되면 해당 Hook을 사용하는 컴포넌트가 다시 렌더링될 수 있다.
또한 active가 false라면 타이머를 생성하지 않는다.
컴포넌트가 제거되거나 설정이 변경되면 기존 타이머를 정리한다.
이러한 구조는 React의 일반적인 useState와 useEffect를 조합한 것이다.
하지만 Hook의 호출 방식이 특이하다.
저자는 반환된 now 값을 사용하지 않는다.
예를 들어 다음과 같이 호출한다.
useNow(1000, hasCountdown)
이 코드에서는 Hook을 실행하지만 반환값을 변수에 저장하지 않는다.
처음 보면 불필요한 Hook 호출처럼 보일 수 있다.
그러나 저자가 원하는 것은 현재 시간을 반환하는 기능이 아니다.
지정한 주기마다 컴포넌트를 다시 렌더링하는 동작 자체다.
서로 다른 세 개의 타이머
Notfio의 세 컴포넌트는 서로 다른 주기를 사용한다.
| 컴포넌트 | 갱신 주기 | 활성화 조건 |
|---|---|---|
| SitesList | 1초 | 재시도 Countdown이 있거나 검색 중 |
| MonitorCard | 5초 | 모니터가 실행 또는 관련 상태에 있음 |
| RecentFinds | 30초 | 최근 발견한 결과가 존재함 |
이러한 차이는 화면에 표시되는 정보의 특성에서 나온다.
예를 들어 재시도까지 45초가 남았다는 Countdown은 1초마다 변경하는 것이 자연스럽다.
반면 마지막 검색이 12초 전이라는 정보는 반드시 매초 변경할 필요가 없을 수 있다.
특히 검색 자체가 30초 간격으로 발생한다면 5초 정도의 갱신 간격도 충분할 수 있다.
최근 발견한 결과는 보통 몇 분 전에 생성된 데이터다.
이러한 데이터는 30초 간격으로 갱신하더라도 사용자에게 큰 차이로 느껴지지 않을 수 있다.
저자는 모든 시간 정보에 동일한 정확도를 요구하지 않고 정보의 특성에 따라 갱신 주기를 결정했다.
반환값을 사용하지 않는 이유
일반적인 React 코드에서는 다음과 같은 구조를 생각할 수 있다.
const now = useNow(1000)
이후 화면의 모든 시간 계산 함수에 now를 전달한다.
하지만 저자는 다른 방식을 선택했다.
상대 시간 Formatter가 직접 Date.now()를 호출하도록 했다.
예를 들어 relativeTime(timestamp) 함수는 호출되는 시점의 현재 시간을 읽어 상대 시간을 계산한다.
useNow()는 컴포넌트가 다시 렌더링될 시점을 결정하는 역할만 한다.
이렇게 하면 현재 시간을 여러 하위 컴포넌트의 Props로 전달할 필요가 없다.
하지만 단점도 존재한다.
relativeTime()은 같은 입력값을 받더라도 호출 시점에 따라 다른 결과를 반환한다.
즉 순수 함수(Pure Function)가 아니다.
또한 현재 시간을 함수 내부에서 직접 읽기 때문에 일반적인 Memoization을 적용하기 어렵다.
테스트에서도 현재 시간을 고정하려면 Clock을 Mocking해야 한다.
저자는 이러한 문제를 인정한다.
하지만 단순히 UI에 상대 시간을 표시하는 함수에서는 해당 트레이드오프를 받아들였다.
반면 점수 계산이나 데이터베이스에 저장할 값을 계산하는 함수에서는 같은 방식을 사용하지 않겠다고 설명한다.
Clock Skew 처리
저자는 실제 서비스에서 시간 차이로 인해 잘못된 값이 표시되는 문제도 경험했다.
예를 들어 Electron의 Main Process와 Renderer Process가 사용하는 시간 정보가 미세하게 다를 수 있다.
이 경우 이벤트가 발생한 시각이 Renderer에서 계산한 현재 시간보다 미래로 표시될 수 있다.
그 결과 다음과 같은 값이 나타날 수 있다.
-3초 전
저자는 이를 방지하기 위해 상대 시간을 계산할 때 Math.max(0, ...)을 사용한다.
이렇게 하면 계산 결과가 음수여도 0으로 처리할 수 있다.
또한 10초 미만의 시간은 정확한 숫자 대신 just now로 표시한다.
예를 들어 1초 전, 2초 전, 3초 전이라는 표시가 계속 변경되면 사용자는 해당 영역을 Stopwatch처럼 느낄 수 있다.
하지만 실제로는 마지막 검색이 방금 이루어졌다는 사실만 전달하면 충분하다.
따라서 지나치게 세밀한 숫자 표시를 줄였다.
이러한 선택은 단순한 시간 계산보다 사용자에게 어떤 수준의 정확도를 전달할지 결정하는 UI 설계와 관련된다.
Countdown의 표현 방식
재시도 Countdown에서도 비슷한 문제가 발생한다.
예를 들어 다음 재시도 시각이 이미 지났다고 가정해 보자.
단순히 남은 시간을 계산하면 음수 값이 표시될 수 있다.
하지만 실제 재시도는 백그라운드 프로세스의 자체 스케줄에 따라 실행된다.
UI가 정확히 어느 밀리초에 재시도가 이루어질지 보장하지는 못한다.
따라서 저자는 남은 시간이 0 이하가 되면 다음 문구를 표시한다.
any moment
이는 곧 실행될 수 있다는 의미다.
또한 1초 미만의 양수 시간이 남아 있을 때 in 0s가 표시되지 않도록 처리했다.
이러한 방식은 실제 스케줄러의 동작과 UI가 표현하는 시간 사이의 차이를 고려한 것이다.
타이머를 비활성화할 때 발생하는 문제
이번 설계의 단점 가운데 하나는 타이머가 중단되면 상대 시간 표시도 오래된 상태로 남을 수 있다는 점이다.
예를 들어 모니터가 중단됐다고 가정해 보자.
더 이상 검색 작업이 실행되지 않는다.
재시도 Countdown도 존재하지 않는다.
이 경우 SitesList의 1초 타이머는 비활성화된다.
하지만 마지막 검색 시점을 표시하는 문장은 그대로 남아 있다.
예를 들어 2분 전이라는 문구가 표시된 상태에서 아무런 이벤트도 발생하지 않으면 5분이 지나도 같은 문구가 유지될 수 있다.
저자는 이러한 문제를 알고 있다.
완벽한 정확성을 원한다면 모니터가 중단된 상태에서도 60초 간격의 느린 타이머를 유지할 수 있다.
그러나 실제 사용자 경험에서 해당 정보의 중요도가 높지 않다고 판단했다.
모니터가 중단된 상태에서는 사용자가 다른 동작을 수행할 때 자연스럽게 다시 렌더링되는 경우가 많기 때문이다.
따라서 저자는 시간 정보의 정확성과 불필요한 렌더링 비용 사이에서 일부 오차를 허용했다.
하나의 전역 Clock을 사용하지 않는 이유
React에서는 Context를 이용해 현재 시간을 전역으로 제공할 수도 있다.
예를 들어 최상위 Provider에서 매초 now를 갱신한다.
이후 하위 컴포넌트가 해당 값을 구독하도록 만들 수 있다.
하지만 이 방식에서는 현재 시간을 구독하는 모든 컴포넌트가 매초 업데이트될 수 있다.
Notifio의 화면에는 최대 200줄의 로그를 표시하는 영역도 존재한다.
로그는 상대 시간 Countdown과 직접적인 관련이 없을 수 있다.
그런데 전역 Clock을 사용하면 관련 없는 컴포넌트까지 반복적으로 렌더링될 가능성이 있다.
저자는 이러한 문제를 피하기 위해 각 컴포넌트가 독립적인 타이머를 사용하도록 했다.
이렇게 하면 SitesList가 1초마다 갱신돼도 RecentFinds가 반드시 같은 주기로 갱신되는 것은 아니다.
다만 전역 Context를 사용한다는 사실만으로 모든 하위 컴포넌트가 무조건 다시 렌더링되는 것은 아니다.
실제 렌더링 범위는 Context 구독 구조와 컴포넌트 구성, Memoization 등에 영향을 받는다.
저자의 선택은 자신의 애플리케이션 구조에서 불필요한 구독과 갱신을 피하기 위한 것이다.
시간 데이터의 실제 의미
저자는 상대 시간 UI를 단순한 문자열이 아니라 시간이 지나면서 정확성이 떨어지는 값으로 설명한다.
예를 들어 12초 전이라는 값은 생성한 순간에는 정확하지만 곧 오래된 데이터가 된다.
따라서 개발자가 결정해야 할 질문은 다음과 같다.
해당 값은 얼마 동안 부정확해도 괜찮은가?
재시도 Countdown이라면 1초 이상 부정확해지는 것이 불편할 수 있다.
반면 오래된 검색 결과의 시각은 30초 정도 늦게 갱신돼도 문제가 없을 수 있다.
이렇게 정확도의 허용 범위를 먼저 결정하면 타이머 주기도 자연스럽게 결정할 수 있다.
이번 글은 React의 렌더링 성능을 개선하기 위해 반드시 복잡한 상태 관리 라이브러리나 Memoization이 필요한 것은 아니라는 점을 보여준다.
특히 상대 시간의 갱신 주기를 화면별로 다르게 설계하고, 불필요한 렌더링을 줄이면서 그로 인해 발생하는 정확도 문제까지 명시적으로 설명한 사례다.
Our design system is a markdown table of nineteen motifs, and the useful column is the one that says no
- 저자: Daniel Pertu
- 발행 매체: DEV Community
- 게시일: 2026-10-22
- 원문: https://dev.to/daniel_pertu/our-design-system-is-a-markdown-table-of-nineteen-motifs-and-the-useful-column-is-the-one-that-fd4
React 애플리케이션의 디자인 시스템을 거대한 컴포넌트 문서가 아니라 19개 시각 요소의 사용 규칙을 담은 Markdown 표로 관리한 방법을 설명한 글이다.
이번 글의 핵심은 디자인 시스템이 단순히 어떤 컴포넌트를 제공하는지 설명하는 문서에 그쳐서는 안 된다는 것이다.
예를 들어 Button 컴포넌트의 색상과 크기를 정의하는 것만으로는 해당 Button을 어떤 상황에서 사용해야 하는지 결정하기 어렵다.
또한 같은 시각 요소가 여러 의미로 사용되면 UI의 일관성이 무너질 수 있다.
저자는 이러한 문제를 해결하기 위해 시각 요소마다 사용할 수 있는 위치와 사용해서는 안 되는 상황을 명시했다.
Nakodo의 디자인 시스템 구조
저자는 Nakodo라는 웹 애플리케이션에서 19개의 시각적 Motif를 사용한다.
Motif는 반복적으로 사용되는 시각적 표현이나 장식 요소를 의미한다.
예를 들어 다음과 같은 요소가 있다.
- Catalogue Number
- Pointer Arrow
- Hatching
- Double Rule
- Stamp
- Live Dot
- Attention Mark
- Tally Mark
이러한 요소는 각각 정해진 의미를 가진다.
예를 들어 Hatching은 아직 관찰되지 않았거나 잠겨 있는 상태를 나타낸다.
반면 단순한 장식 목적으로는 사용하지 않는다.
Live Dot은 상태를 나타내는 특정한 모양이다.
다른 상태 요소가 동일한 모양을 사용하지 않도록 제한한다.
이러한 규칙은 docs/brand.md에 Markdown 표로 기록돼 있다.
표에는 다음 정보가 포함된다.
- Motif 이름
- 어떤 시각 요소인지
- 어디에서 사용하는지
- 어떤 상황에서 사용하면 안 되는지
저자는 특히 마지막 열이 중요하다고 설명한다.
디자인 시스템은 무엇을 할 수 있는지뿐 아니라 무엇을 해서는 안 되는지 정의해야 한다는 것이다.
TypeScript로 강제할 수 있는 규칙
일부 디자인 규칙은 TypeScript의 타입 시스템으로 강제할 수 있다.
예를 들어 Stamp 컴포넌트에서는 두 가지 색상만 허용한다.
inkcyano
타입은 다음과 같이 정의된다.
tone?: "ink" | "cyano"
이렇게 하면 개발자가 다른 색상 이름을 전달했을 때 TypeScript 오류가 발생한다.
예를 들어 빨간색이나 노란색을 전달하려고 한다면 허용된 타입과 일치하지 않는다.
이는 코드 리뷰에서 해당 사용법이 잘못됐는지 사람이 매번 확인하는 방식과 다르다.
사용해서는 안 되는 색상을 컴포넌트의 공개 API에서 아예 표현할 수 없도록 만든 것이다.
또한 Stamp의 회전 각도도 Props로 제공하지 않는다.
회전 각도는 컴포넌트 내부에 고정돼 있다.
이렇게 하면 개발자가 임의로 과도한 회전을 적용하는 상황을 방지할 수 있다.
Catalogue Number의 형식 제한
Catalogue Number에도 비슷한 규칙을 적용한다.
저자의 디자인 시스템에서는 Creator와 Guide에 서로 다른 숫자 형식을 사용한다.
| 종류 | 표시 형식 |
|---|---|
| Creator | 네 자리 숫자 |
| Guide | 두 자리 숫자 |
예를 들어 Creator의 번호가 7이라면 다음과 같이 표시한다.
No. 0007
Guide의 번호가 7이라면 다음과 같이 표시한다.
Guide 07
저자는 각 컴포넌트에서 직접 숫자를 문자열로 변환하도록 하지 않는다.
대신 CatalogueNo 컴포넌트에서 종류에 따라 숫자 형식을 결정한다.
이렇게 하면 같은 의미의 데이터가 서로 다른 화면에서 다른 방식으로 표시되는 문제를 줄일 수 있다.
예를 들어 어떤 화면에서는 No. 7, 다른 화면에서는 No. 0007로 표시되는 상황을 방지할 수 있다.
CSS로 구현한 시각적 규칙
일부 디자인 규칙은 TypeScript보다 CSS로 표현하는 편이 적합하다.
대표적인 예가 Specimen Tag다.
Specimen Tag는 왼쪽 모서리가 잘린 작은 Label이다.
또한 구멍이 뚫린 것처럼 보이는 시각 효과가 있다.
저자는 이를 여러 이미지 파일로 구현하지 않았다.
대신 CSS의 clip-path를 사용했다.
clip-path는 요소가 표시되는 영역의 모양을 지정할 수 있는 CSS 속성이다.
예를 들어 polygon()을 사용하면 요소를 특정 다각형 모양으로 잘라 표시할 수 있다.
저자는 Label의 왼쪽 모서리를 다각형으로 잘라냈다.
구멍처럼 보이는 작은 원은 별도의 요소로 구현했다.
하지만 실제로 요소를 투명하게 만든 것은 아니다.
배경색과 같은 색상을 적용해 구멍이 뚫린 것처럼 보이도록 했다.
이러한 구현에는 제약이 존재한다.
Label이 항상 정해진 배경색 위에 표시된다는 가정이 필요하기 때문이다.
다른 배경 이미지나 색상 위에 표시하면 구멍 효과가 자연스럽지 않을 수 있다.
따라서 이 규칙은 CSS 구현뿐 아니라 디자인 시스템의 사용 위치 제한과도 연결된다.
Windows 고대비 모드 대응
저자는 Hatching에도 별도의 접근성 처리를 추가했다.
Hatching은 반복적인 선 패턴을 이용해 특정 상태를 표현하는 시각 요소다.
예를 들어 잠긴 기능이나 아직 사용할 수 없는 요소에 적용할 수 있다.
하지만 Windows의 고대비 모드에서는 CSS의 배경 이미지와 Gradient가 정상적으로 표시되지 않을 수 있다.
이 경우 Hatching을 통해 전달하던 의미가 사라진다.
저자는 이를 해결하기 위해 forced-colors 미디어 쿼리를 사용했다.
forced-colors는 브라우저가 운영체제의 강제 색상 모드에서 실행되는지 확인할 수 있도록 하는 기능이다.
해당 모드에서는 Hatching의 배경 이미지를 제거한다.
대신 다음과 같은 Border를 사용한다.
1px dashed CanvasText
CanvasText는 시스템 색상을 나타내는 CSS 값이다.
따라서 사용자가 선택한 고대비 색상 체계와 연결된다.
이러한 방식은 단순히 시각적인 효과를 유지하는 데 그치지 않는다.
색상과 패턴이 달라져도 해당 요소가 잠긴 상태라는 의미를 유지하는 것이 목적이다.
또한 Hatching은 텍스트와 함께 사용하도록 규칙을 정의했다.
따라서 사용자가 시각적 패턴을 인식하지 못하더라도 텍스트로 상태를 이해할 수 있도록 한다.
25개 이하에서는 Tally Mark 사용
저자의 디자인 시스템에서는 작은 사용량을 표현할 때 Tally Mark를 사용한다.
Tally Mark는 개수를 셀 때 사용하는 막대 표시다.
예를 들어 다섯 개를 나타낼 때 세로선 네 개와 대각선 하나를 사용할 수 있다.
하지만 항목의 수가 매우 많아지면 Tally Mark를 직접 세는 것이 어려워진다.
따라서 저자는 다음 기준을 정의했다.
최대 25개까지 Tally Mark를 사용하고, 25개를 초과하면 일반적인 Progress Bar로 전환한다.
예를 들어 하루에 사용할 수 있는 기능이 20회라면 Tally Mark로 현재 사용량을 표시할 수 있다.
반면 사용량 한도가 1,000회라면 Tally Mark보다 숫자와 Progress Bar를 사용하는 편이 읽기 쉽다.
이러한 기준은 단순한 시각적 취향이 아니다.
사용자가 실제 값을 읽고 이해하는 방식과 관련된다.
SVG Stroke Animation
Tally Mark는 SVG로 구현했다.
SVG는 벡터 그래픽을 표현하기 위한 형식이다.
저자는 각 선을 Path로 정의했다.
그리고 다음 속성을 사용한다.
pathLength={1}
pathLength를 1로 설정하면 각 Path의 길이를 정규화해 애니메이션을 적용하기 쉬워진다.
예를 들어 짧은 세로선과 긴 대각선은 실제 길이가 다르다.
하지만 각 Path의 길이를 같은 기준으로 표현하면 동일한 stroke-dasharray와 stroke-dashoffset 설정을 사용할 수 있다.
저자는 각 선에 순서 번호를 부여했다.
그리고 선마다 45ms의 지연 시간을 적용해 순차적으로 그려지는 효과를 만들었다.
이렇게 하면 사용량을 나타내는 선이 하나씩 나타나는 애니메이션을 구현할 수 있다.
또한 화면 밖에 있는 요소에서는 애니메이션을 바로 실행하지 않는다.
실제로 해당 요소가 화면에 들어왔을 때 실행하도록 구성했다.
다만 이러한 애니메이션이 항상 필요한 것은 아니다.
움직임을 줄여야 하는 사용자에게는 운영체제의 접근성 설정에 맞춰 애니메이션을 제한하는 방식도 고려해야 한다.
코드만으로 강제할 수 없는 규칙
저자는 모든 디자인 규칙을 컴포넌트로 강제할 수 없다는 점도 인정한다.
예를 들어 다음 규칙이 있다고 가정해 보자.
한 화면에서 Double Rule은 최대 하나만 사용한다.
개별 Double Rule 컴포넌트는 자신이 현재 페이지에서 몇 번 사용되는지 알기 어렵다.
다른 컴포넌트나 Layout에서 같은 요소를 사용하고 있을 수 있기 때문이다.
따라서 이 규칙을 완전히 강제하려면 전체 페이지 구조와 라우팅 정보를 분석해야 할 수 있다.
저자는 이를 위한 복잡한 Lint 규칙을 만들지 않았다.
대신 시각 요소의 구현과 이름을 통일했다.
그러면 실제 페이지에서 해당 요소가 몇 번 사용됐는지 확인하기 쉬워진다.
실제 HTML 검사
저자는 실제 운영 중인 웹페이지를 대상으로 Motif의 사용 횟수를 확인했다.
예를 들어 특정 SVG Path 문자열이나 CSS 클래스 이름을 검색했다.
검사에는 일반적인 curl과 grep을 사용했다.
이렇게 하면 브라우저에서 직접 화면을 보지 않아도 특정 시각 요소의 사용 횟수를 확인할 수 있다.
예를 들어 홈페이지에 Catalogue Number가 일곱 개 존재하는지 확인할 수 있다.
또한 가격 페이지에 사용하면 안 되는 Plate Mark가 포함되지 않았는지 검사할 수 있다.
이 과정에서 저자는 사용하지 않는 공통 클래스와 예상하지 못한 컴포넌트 사용 위치도 발견했다.
이는 디자인 시스템의 규칙이 명확하다면 단순한 텍스트 검사만으로도 일부 사용 패턴을 점검할 수 있다는 사례다.
디자인 규칙의 책임 분리
이번 글에서 제시하는 규칙은 크게 세 가지로 구분할 수 있다.
첫 번째는 TypeScript가 강제할 수 있는 규칙이다.
예를 들어 허용된 색상이나 컴포넌트 종류가 해당한다.
두 번째는 CSS가 표현할 수 있는 규칙이다.
예를 들어 모양과 간격, 고대비 모드의 대체 표현이 해당한다.
세 번째는 코드만으로 강제하기 어려운 사용 맥락이다.
예를 들어 하나의 화면에서 특정 시각 요소를 몇 번 사용해야 하는지에 관한 규칙이다.
저자는 이러한 모든 문제를 하나의 거대한 컴포넌트 시스템으로 해결하려고 하지 않았다.
대신 강제할 수 있는 규칙은 코드에 반영하고, 강제하기 어려운 규칙은 명확한 문서와 검토 가능한 구조로 남겼다.
이번 글은 디자인 시스템을 단순한 UI 컴포넌트 모음이 아니라 제품의 시각적 표현에 일관된 의미를 부여하고, 그 의미가 잘못 사용되지 않도록 제한하는 규칙 체계로 설계한 사례다.
Localizing compliance at scale: achieving South Korea’s ISMS certification
- 저자: Kabin Calan / Cloudflare
- 게시일: 2026-10-22
- 원문: https://blog.cloudflare.com/k-isms/
Cloudflare가 한국의 ISMS 인증을 획득하는 과정에서 글로벌 웹서비스의 회원가입 UI와 개인정보 처리 흐름을 어떻게 변경했는지 설명한 기술·운영 사례다.
이번 글은 단순히 기업이 새로운 보안 인증을 획득했다는 발표에 그치지 않는다.
특히 한국 사용자를 대상으로 하는 웹서비스에서 개인정보 동의를 어떻게 구성해야 하는지, 외부 소셜 로그인과 국가 판별 로직을 어떻게 처리해야 하는지 실제 제품 변경을 중심으로 설명한다.
한국에서 서비스를 운영하는 프론트엔드 개발자에게도 관련성이 높은 내용이다.
글로벌 회원가입 UI와 한국 회원가입 UI의 차이
Cloudflare의 기존 글로벌 회원가입 화면에서는 서비스 약관과 개인정보 처리방침 등에 동의한다는 안내 문구를 하나로 묶어 제공했다.
하지만 한국 사용자를 위한 회원가입 화면에서는 이러한 방식을 변경했다.
Cloudflare는 동의 항목을 다음 네 가지로 구분했다.
| 동의 항목 | 필수 여부 |
|---|---|
| 서비스 약관 | 필수 |
| 개인정보 수집 및 이용 | 필수 |
| 제한된 범위의 개인정보 해외 이전 | 필수 |
| 마케팅을 위한 제한된 범위의 개인정보 제3자 제공 | 선택 |
각 항목에는 해당 동의 내용의 전문을 확인할 수 있는 링크가 연결된다.
또한 모든 항목에 한 번에 동의할 수 있는 모두 동의 기능도 제공한다.
여기서 중요한 점은 필수 항목과 선택 항목을 명확하게 구분했다는 사실이다.
사용자는 선택 항목에 동의하지 않더라도 계정을 생성할 수 있다.
Cloudflare는 선택 항목에 동의하지 않았다는 이유로 회원가입을 제한하지 않는다.
반면 세 가지 필수 항목에 모두 동의하지 않았다면 회원가입 버튼이 활성화되지 않는다.
따라서 각 동의 항목은 실제 회원가입 가능 여부와 연결된다.
이는 단순히 Checkbox를 네 개 추가한 UI 변경이 아니다.
동의 여부에 따라 회원가입을 허용하는 비즈니스 규칙이 변경된 것이다.
소셜 로그인에서 발생한 문제
Cloudflare의 글로벌 서비스에서는 다음 소셜 로그인 기능을 사용할 수 있다.
- Apple
- GitHub
소셜 로그인은 사용자가 기존 계정을 이용해 새로운 서비스에 가입할 수 있도록 하는 기능이다.
하지만 이번 한국 회원가입 UI에서는 문제가 발생했다.
일반적인 소셜 로그인 흐름에서는 사용자가 Cloudflare의 자체 회원가입 양식을 거치지 않을 수 있다.
예를 들어 사용자가 Google로 계속하기 버튼을 클릭하면 Google의 인증 화면으로 이동한다.
인증이 완료되면 Cloudflare로 돌아와 계정이 생성될 수 있다.
이 과정에서는 Cloudflare가 직접 구성한 네 가지 개인정보 동의 항목이 표시되지 않을 수 있었다.
즉 사용자가 자체 회원가입 화면을 건너뛰면서 필수 동의 절차도 건너뛰는 문제가 발생한다.
Cloudflare는 두 가지 방법을 검토했다.
첫 번째는 소셜 로그인 이후 별도의 동의 화면을 추가하는 방식이다.
두 번째는 한국 사용자의 회원가입에서 소셜 로그인을 제공하지 않는 방식이다.
최종적으로 두 번째 방법을 선택했다.
한국에서는 Google·Apple·GitHub 소셜 회원가입을 제공하지 않도록 변경한 것이다.
이렇게 하면 모든 신규 계정이 Cloudflare가 직접 관리하는 회원가입 양식을 통과하도록 할 수 있다.
다만 이 선택이 모든 한국 서비스에 반드시 적용해야 하는 유일한 구현 방식이라는 의미는 아니다.
소셜 인증 이후 별도의 동의 절차를 제공하는 방식도 설계할 수 있다.
이번 글은 Cloudflare가 자신의 서비스 구조와 인증 흐름을 고려해 선택한 구현 방법을 설명한다.
IP Geolocation을 이용한 국가 판별
Cloudflare는 사용자의 국가를 판단하기 위해 IP Geolocation 정보를 활용한다.
IP Geolocation은 사용자의 IP 주소를 바탕으로 대략적인 국가나 지역을 추정하는 기술이다.
Cloudflare는 이러한 정보를 HTTP 요청에 제공한다.
대표적인 정보는 다음과 같다.
CF-IPCountryrequest.cf.country
CF-IPCountry는 Origin 서버에 전달되는 요청 Header를 통해 사용할 수 있다.
Cloudflare Worker에서는 request.cf.country를 통해 국가 정보를 확인할 수 있다.
예를 들어 국가 코드가 KR이라면 한국 사용자를 위한 회원가입 UI를 기본적으로 제공할 수 있다.
이러한 방식은 별도의 외부 Geolocation API를 호출하지 않아도 된다는 장점이 있다.
Cloudflare의 네트워크가 이미 판단한 국가 정보를 활용하기 때문이다.
하지만 여기에는 중요한 문제가 있다.
사용자의 현재 IP 주소가 사용자의 실제 거주 국가를 정확하게 나타내지는 않는다.
예를 들어 한국에 거주하는 사용자가 해외여행 중일 수 있다.
또는 VPN을 사용하고 있을 수 있다.
이 경우 해당 사용자의 IP 주소는 한국이 아닌 다른 국가로 판별될 수 있다.
사용자가 선택한 국가를 우선하는 구조
Cloudflare는 이러한 한계를 해결하기 위해 IP 주소를 유일한 판단 기준으로 사용하지 않았다.
IP Geolocation은 처음 회원가입 화면을 표시할 때 기본 국가를 선택하는 데 사용한다.
하지만 사용자가 회원가입 과정에서 직접 국가를 선택하면 사용자가 선택한 국가를 우선 적용한다.
예를 들어 한국에 거주하는 사용자가 미국에서 접속했다고 가정해 보자.
IP Geolocation에서는 미국으로 판별될 수 있다.
하지만 사용자가 회원가입 화면에서 한국을 선택하면 한국의 동의 양식을 표시하도록 한다.
반대로 IP 주소가 한국으로 판별됐더라도 사용자가 다른 국가에 거주한다면 직접 국가를 선택할 수 있도록 한다.
이는 네트워크에서 추정한 정보와 사용자가 명시적으로 제공한 정보를 구분하는 설계다.
Cloudflare는 다음 두 가지를 같은 개념으로 취급해서는 안 된다고 설명한다.
- 요청이 어느 국가의 IP 주소에서 왔는가
- 사용자가 실제로 어느 국가에 거주하는가
이 차이는 회원가입뿐 아니라 세금 계산, 서비스 이용 가능 지역, 개인정보 처리 정책을 설계할 때도 고려할 수 있다.
다만 실제로 어떤 국가별 정책을 적용해야 하는지는 해당 서비스의 법적 요구사항에 따라 달라진다.
UI 변경만으로 끝나지 않는 인증 흐름
이번 구현에서 중요한 부분은 회원가입 화면에 Checkbox를 추가하는 작업만으로는 충분하지 않았다는 점이다.
서비스에는 여러 계정 생성 경로가 존재했다.
예를 들어 일반적인 이메일 회원가입과 외부 소셜 로그인은 서로 다른 흐름이다.
특정 동의 절차를 필수로 만들려면 실제로 계정이 생성되는 모든 경로에서 해당 조건을 충족해야 한다.
Cloudflare는 이를 해결하기 위해 계정 생성 경로를 함께 변경했다.
이러한 접근은 프론트엔드 개발에서도 중요하다.
사용자 인터페이스에서 특정 동작을 막았다고 해서 서버에서 해당 동작을 수행할 수 없는 것은 아니기 때문이다.
예를 들어 버튼을 비활성화하더라도 클라이언트는 HTTP 요청을 직접 구성할 수 있다.
따라서 중요한 비즈니스 규칙은 실제 서버와 인증 시스템에서도 검증해야 한다.
이번 글은 회원가입 화면과 계정 생성 절차가 서로 분리된 문제가 아니라는 점을 보여준다.
ISMS 인증에서 확인한 사항
ISMS(Information Security Management System)는 정보보호 관리체계에 관한 인증이다.
Cloudflare는 한국의 개인정보 및 정보보호 관련 요구사항을 충족하기 위해 ISMS 인증을 준비했다.
공식 글에 따르면 인증 준비에는 약 1년 6개월이 소요됐다.
감사 과정에서는 KISA와 관련된 감사인이 약 10일 동안 80개의 통제 항목을 확인했다.
검토 대상에는 다음과 같은 영역이 포함됐다.
- 접근 권한 관리
- 인증 및 계정 관리
- 암호화
- 사고 대응
- 위험 관리
- 재해 복구
- 개인정보 처리 흐름
- 개인정보에 접근할 수 있는 내부 시스템
- 회원가입 과정의 동의 문구와 UI
Cloudflare는 기존에도 ISO 27001과 여러 국제 보안 인증을 보유하고 있었다.
하지만 이러한 인증이 한국의 요구사항을 자동으로 충족하는 것은 아니었다.
특히 이번 인증 과정에서는 개인정보가 어떤 시스템을 거쳐 이동하고, 어떤 사용자가 접근할 수 있는지 구체적인 자료를 요구받았다.
Data Flow Diagram의 중요성
감사 과정에서 중요한 역할을 한 자료는 Data Flow Diagram이었다.
Data Flow Diagram은 데이터가 시스템 안에서 어떤 경로로 이동하는지 나타내는 다이어그램이다.
예를 들어 사용자가 회원가입을 수행하면 다음과 같은 흐름이 발생할 수 있다.
- 브라우저에서 회원가입 정보 입력
- 서버로 개인정보 전달
- 인증 시스템에서 계정 생성
- 데이터베이스에 필요한 정보 저장
- 내부 운영 시스템에서 일부 정보 조회
- 필요에 따라 다른 시스템으로 데이터 전달
- 보존 기간 이후 데이터 삭제
각 단계에서는 데이터가 어떻게 보호되는지 확인해야 한다.
Cloudflare는 단순히 시스템의 동작을 문장으로 설명하는 것보다 실제 데이터 이동 경로를 다이어그램으로 표현하는 것이 중요했다고 설명한다.
감사인은 해당 다이어그램을 바탕으로 접근 제어와 실제 운영 절차를 확인했다.
또한 데이터가 처음 수집되는 시점부터 삭제되는 시점까지 전체 수명주기를 검토했다.
이는 웹 개발에서 개인정보를 단순히 데이터베이스의 일부 필드로만 생각해서는 안 된다는 점과 연결된다.
실제로는 개인정보가 어떤 경로를 통해 저장되고 조회되며 삭제되는지 전체 시스템 차원에서 관리해야 한다.
감사 범위와 서비스 경계
Cloudflare는 ISMS 인증의 적용 범위를 개별 제품 이름의 목록으로 정의하지 않는다고 설명한다.
대신 인증된 서비스의 경계를 기준으로 판단한다.
이 경계에는 다음 요소가 포함된다.
- 서비스를 운영하는 조직과 절차
- 서비스를 제공하는 네트워크와 인프라
- 관리 대상 정보자산
- 개인정보가 처리되는 경로
따라서 특정 Cloudflare 제품이 인증 범위에 포함되는지는 해당 제품이 인증된 서비스 경계 안에서 제공되는지에 따라 판단해야 한다.
이는 제품 이름이 인증서에 직접 표시되지 않았다고 해서 해당 제품이 무조건 인증 범위 밖이라는 의미는 아니라는 설명이다.
하지만 반대로 Cloudflare의 모든 제품이 별도의 확인 없이 동일한 범위에 포함된다고 단정할 수도 없다.
실제 사용 중인 서비스가 인증된 경계에 포함되는지 확인해야 한다.
운영상의 한계
이번 글은 Cloudflare가 직접 수행한 인증 준비와 제품 변경 과정을 설명한다.
따라서 다른 기업이 같은 네 가지 Checkbox와 같은 회원가입 정책을 구현하면 반드시 동일한 법적 요구사항을 충족한다는 의미는 아니다.
실제 개인정보 처리 방식과 서비스 구조, 적용되는 규정에 따라 필요한 동의 항목과 처리 절차가 달라질 수 있다.
또한 소셜 로그인을 비활성화하는 방식은 Cloudflare가 선택한 방법이다.
다른 서비스에서는 소셜 로그인 이후 별도의 동의 절차를 제공하는 방식이 적합할 수도 있다.
이번 글의 의미는 특정 UI를 그대로 복사하는 데 있지 않다.
글로벌 웹서비스에서 국가별 규제 요구사항을 실제 사용자 흐름과 인증 과정, 데이터 이동 구조에 어떻게 반영하는지를 설명한다는 점에 있다.
특히 IP Geolocation의 불확실성을 인정하고 사용자가 명시적으로 선택한 정보를 우선하는 설계, 소셜 로그인처럼 기존 동의 화면을 우회하는 경로를 점검한 과정이 구체적이다.
한국 사용자를 대상으로 웹서비스를 설계하거나 글로벌 서비스의 회원가입 및 개인정보 처리 흐름을 구현할 때 참고할 수 있는 사례다.