designpaca/apps/site/design.md
Yun Chan e1e6002bee
All checks were successful
ci / build (push) Successful in 25s
backdrop-filter 오판을 정정하고 리퀴드 글래스 기법을 기록한다
'backdrop-filter 에 SVG 를 얹으면 Chrome 이 선언 전체를 버린다' 고
적어두고 페이지 프리플라이트 로그에까지 실었다. 사실이 아니었다.

Chrome 151 에서 재측정
  computed 값이 none 이 아니라 url(#id) 로 유지된다
  대비 높은 배경 위에서 실제로 굴절된다(줄무늬가 가장자리에서 압축됨)

진짜 원인은 feImage 의 width/height 를 요소 크기와 맞추지 않은 것이었다.
backdrop-filter 는 필터 이미지를 자동으로 조정하지 않는다.

검색도 다시 했다. feTurbulence grain 에 갇혀 2019년 글만 보고 있었다.
liquid glass 계열 최신 자료를 보니 변위 지도는 노이즈가 아니라
물리 기반 사전계산 이미지였다 — R=X변위, G=Y변위, 128 이 중립,
감쇠는 Squircle 곡선. 캔버스로 즉석 생성하면 240x160 이 2.7KB 다.

스킬 svg-filters.md 에 '리퀴드 글래스' 절을 넣었다.
구조 · 변위 지도 만드는 법 · 함정 넷 · @supports 폴백.
오판 정정도 문서 첫 줄에 남겼다.

한 번 실패한 것을 '안 되는 것' 으로 적으면 그 문장이 다음 사람의
상한선이 된다. 페이지 로그 항목도 그 내용으로 바꿨다.
2026-08-20 22:09:22 +09:00

33 KiB
Raw Blame History

design.md — designpaca 소개 페이지

6단계 산출물. 다음 실행(사람이든 에이전트든)은 이 파일을 먼저 읽고 같은 결정을 이어간다.

개정 이력

날짜 무엇이 바뀌었나
2026-08-20 초판 swiss-minimal · three.js 미사용 · 라이트 기반
2026-08-20 2판(현행) dark-instrument · three.js 채택 · 전체 경로 재주행

초판이 거짓말이 됐던 경위를 남긴다. 사용자가 "글래스모피즘·리퀴드글래스·히어로에 힘"을 요청했을 때 그것은 2단계 방향 재결정이었고, 스킬은 그럴 때 전체 경로로 올려 1단계부터 다시 돌라고 씌어 있다. 그러지 않고 4단계 구현으로 바로 들어갔다. 레퍼런스 없이 재질만 갈아끼운 결과가 스위스 미니멀 뼈대에 유리를 바른 페이지였고, design.md 는 초판 상태로 남아 프리셋·이펙트·형태 수치가 전부 어긋났다. 2판은 0단계부터 다시 돌았다.


브리프 3줄

  • 무엇을: designpaca 소개 페이지. 파이프라인 스킬 + npx 설치 CLI 를 소개하는 단일 페이지, 한국어·영어 두 판
  • 누구에게: Claude Code · Codex · Cursor 를 쓰는 개발자. 이 페이지를 보고 npx designpaca 를 터미널에 치게 만드는 것이 목표
  • 제약: Astro 정적 → Cloudflare Pages. 이 페이지 자체가 스킬의 품질 증거물이다 — 스킬이 금지하는 것을 이 페이지가 하면 안 된다

레퍼런스 (전부 실측. computed style 추출까지 돌렸다)

슬롯 사이트 업종 실측값 가져온 원리 가져오지 않은
R1 · 구조 zed.dev 코드 에디터 컨테이너 1120px(68회) ↔ 전체폭 1425px(50회) 교대 · 본문 칼럼 512px · radius 2px 가 621회 · 모노 524회로 지배 폭의 교대, 각진 형태 어휘, 모노를 정체성으로 세리프 h1(우리는 한글) · 최대÷본문 3.0배(위계가 약하다) · 섹션 11개
R2 · 톤 lusion.co 3D 프로덕션 스튜디오 배경 흰색 · 텍스트 색 2단계(검정 135 / 흰 16) · 폰트 1개(Aeonik) · 강조색 rgb(0,22,236) 4회 · 캔버스는 radius 15px 액자 안 · h1 이 36px 3D 는 액자 안, 텍스트는 액자 밖. 3D 가 크면 타이포는 물러난다. 색은 캔버스 안에서만 소리친다 흰 배경 자체 · 파랑
R3 · 디테일 Codrops "Inside HAOQI.DESIGN" 축 5 전용 DOM 이 구조를 소유하고 WebGL 은 셰이더 uniform 만 만진다. 한 rAF 에서 스크롤을 공유하고 뷰포트 밖에서는 정지 R3F/Lenis 스택

R3 구현 제약: 셰이더 uniform 만 갱신하므로 transform/opacity 외의 CSS 를 애니메이션하지 않는다(게이트 #8 안전). 뷰포트 밖 렌더 정지가 필수 조건이다.

접근 실패 기록iglooinc.com 은 hugedomains 파킹으로 302, lasvit.com 은 403. reference-method.md §3-b 가 경고한 그대로였다. 왕복 세 번에서 끊고 우회했다.

히어로 국소 레퍼런스 (1)

R1~R3 로 구조를 잡은 뒤에도 히어로가 어색해서 국소 브리프 표로 두 곳을 더 실측했다. design.md 는 "3D 는 액자 안"까지만 답하고 액자가 화면의 몇 %여야 하는지는 답하지 않는다.

사이트 3D 영역 텍스트 위치 h1
lusion.co 1440×645 = 첫 화면의 71% 캔버스 , 조밀하게 36px
basement.studio 1425×900 = 100% 캔버스 아래(y=916) 87px, weight 600
railway.com SVG 1062×755, h1 아래 중앙 상단 54px, 1줄
우리(고치기 전) 1352×465 = 52% 위 380px 41.5px

셋 다 3D 와 텍스트를 세로로 완전히 분리하고, 3D 가 첫 화면의 70% 이상을 먹는다. 우리는 텍스트 43% / 3D 52% 로 둘 다 어중간했다. basement 형(첫 화면 전체 3D)은 CTA 가 첫 화면에서 밀려나므로 전환이 목적인 우리에게 맞지 않는다. lusion 비율을 택했다.

현행: 텍스트 24% · 액자 59% · 캡션과 링크가 액자 아래 한 줄. 링크(저장소·npm)를 텍스트 블록에서 액자 캡션 줄로 내려 상단을 비웠다.

레퍼런스를 보기 전에 네 번 고쳤다. 판 폭 7.4 → 15, 액자 비율 16/7 → 16/5.5 → 16/6.6 → 16/6, sway 0.5 → 0.9 → 0.16. 전부 계산이지 근거가 아니었다. 레퍼런스 두 곳을 재자 한 번에 정리됐다.

한 문장 컨셉

검사대 위의 물체. 액자가 게이트고, 안에 든 것이 검사 중인 디자인이고, 밖의 글자가 판정 기록이다.

프리셋 — dark-instrument

초판의 swiss-minimal 에서 바꿨다. 페이지가 이미 어둡고 제품이 CLI 다. 초판에는 이 재질을 정당화하는 프리셋이 아예 없었다 — 그것이 "유리를 발랐는데 유리일 이유가 없다"의 원인이다.

프리셋은 "글래스모피즘은 내비게이션 바 정도까지"라고 반대한다. 사용자의 명시적 지시가 프리셋보다 위이므로 없애지 않고 의미가 생기는 자리로 옮겼다: 유리는 액자에만 쓴다 — 헤더 · 검사대 프레임 · 명령창. 콘텐츠 카드(브리프, 조판 데모)에서는 뺐다. 유리는 무언가를 담을 때만 의미가 있다.

감수한 리스크 — 하나

히어로에서 거대 타이포를 버리고 힘을 3D 액자로 넘긴다. 초판 히어로의 --display 는 최대 112px 로 화면 높이의 절반을 먹었고, 그 아래 캔버스가 깔려 있었다. R2 의 h1 은 36px 인데 히어로가 압도적이다 — 힘이 타이포가 아니라 물체에서 나오기 때문이다. 현행 히어로 h1 은 clamp(var(--step-2), 1.1rem + 2.4vw, var(--step-4)), 최대 41.5px 다. 페이지에서 가장 큰 글자(96px)는 히어로가 아니라 수치 섹션에 있다 — R2 도 그렇다.

토큰

src/styles/tokens.css 전문. 초판에서 바뀐 것만 적는다.

  • radius 2종: --radius-sm: 3px(요소) · --radius-frame: 14px(액자). 근거는 R1 의 2px(621회)과 R2 의 15px. 999px pill 3곳을 없앴다 — 계기판에 알약은 없다
  • --glow-2: #6E7BFF#3D7BD6. 앞의 값은 R 이 높아 보라-파랑으로 읽힌다. antipatterns.md 의 슬롭 지문이자 dark-instrument 가 "어두운 배경일수록 빠지기 쉬운 함정"으로 지목한 색이다
  • 유리 색값을 전부 토큰으로: --glass-sheen · --glass-rim · --core-hi. base.css 에 흩어져 있던 rgb(255 255 255 / …) 7곳이 하드 게이트 #1 위반이었다
  • 이중 베젤(.shell/.core) 폐기 → 단일 .frame. 껍데기와 속의 radius 가 서로 달라 그것만으로 형태 어휘가 두 종 늘어난다
  • --step--1: 0.8125rem 추가. 게이트 판독기가 스케일 밖 13px 을 쓰고 있었다
  • 강조색은 여전히 딥 틸 하나. 라이트/다크 쌍(--accent / --accent-inverse)은 규칙으로 정의된 반전이라 1종으로 센다

타입 스케일 실측: step--1 ~ step-46단계, --display(96px)는 스케일 밖, 모노는 본문의 0.9375배 파생. 최대÷본문 = 5.6배. 초판 페이지는 "5단계"라고 실었는데 실측은 9종이었다 — 아래 8·9번 참조.

성능 — 예산과 실측

예산을 올렸고, 올렸다고 말한다. 초판 예산은 히어로까지 JS 50KB 였다. 이 페이지는 three.js 를 파는 제품의 데모이므로 그 판단을 검증할 수 있어야 한다.

예산 숫자 자체를 한 번 고쳤다. 처음 three.js 예산을 160KB 로 적었는데 실측이 184KB 였다. three.js 크기는 미리 알 수 있는 값이므로 이건 예산 설정 실패다. 190KB 로 고쳤고, 사후 조정이라는 사실을 남긴다. 실측을 보고 예산을 맞추는 것은 예산이 아니다 — 다음 실행은 여기서부터 시작해야 한다.

항목 예산 실측 판정
히어로까지 JS, 초기 (gzip) 50KB 복사 버튼 314B + 씬 부트 통과
three.js, 지연 로드 (gzip) 190KB 184KB (동적 import(), IntersectionObserver 로 화면에 들어올 때만) 통과
첫 인터랙션 2초 미측정 배포 후 Lighthouse
LCP / CLS 2.5초 / 0.1 미측정 배포 후

WebGL 게이트: prefers-reduced-motion 이면 아예 시작하지 않는다. DPR 1.5 클램프. hardwareConcurrency <= 4 면 판을 6장에서 3장으로 줄인다. 탭이 숨거나 화면 밖이면 그리지 않는다.

채택한 이펙트와 폴백

이펙트 구현 폴백
검사대 3D 유리판 6장이 깊이 축에 서고 카메라가 스크롤로 통과. 굴절은 렌더 타겟 1장 샘플링(transmission 재질보다 훨씬 싸다) 캔버스 아래 CSS 그라디언트가 폴백이 아니라 결과물이다. 액자·판독기·캡션은 그대로 남는다
게이트 판독기 통과한 판이 바뀔 때만 DOM 을 쓴다. 프레임마다 쓰지 않는다 JS 없으면 게이트 01 / 06 정적 표시
복사 버튼 인라인 314B 클립보드 실패 시 텍스트를 선택 상태로 만든다

5단계에서 실제로 걸린 것

정적 검사와 실렌더를 둘 다 돌렸다. 걸린 것을 전부 적는다 — 유리한 것만 고르면 리스크가 자기 홍보가 된다.

  1. 하드 게이트 #3 위반 — radius 4종(sm/md/shell/999px). 2종으로 정리
  2. 하드 게이트 #1 위반base.css 에 토큰 밖 rgb() 7곳. 토큰으로 올림
  3. 하드 게이트 #7 위반 — 헤더 "English" 버튼 2.59:1. --ink-muted--ink 로 올려 통과
  4. 카운트 규칙 위반 — 히어로 h1 이 4줄이었다(상한 2줄). 폰트 크기를 낮춰 2줄
  5. 셰이더 결함 — 판이 정면으로 서 있어 dot(n, view) ≈ 1, 프레넬이 구조적으로 0이었다. 유리로 읽히게 하는 유일한 신호가 죽어 있었고 그것이 "그냥 흐릿한 사각형"의 원인이다. 법선을 미세하게 굽히고 UV 경계로 테두리 광을 따로 만들어 해결
  6. 셰이더 UV 오류gl_FragCoord 를 렌더 타겟 크기(캔버스의 0.6배)로 나누고 있었다. UV 가 1 을 넘어 텍스처가 잘렸다. getDrawingBufferSize() 로 교정
  7. 폭 리듬 부재 — 9개 섹션 중 파이프라인이 1440px 중 625px(43%) 만 쓰면서 높이는 1637px 로 가장 길었다. 페이지의 본체가 가장 좁았다. 2열로 열어 1113px
  8. 페이지가 실은 수치가 틀렸다 — "타입 단계 5단계" 라고 실었는데 실측 9종이었다. 이 페이지의 리스크가 "자기 수치를 공개한다" 인데 공개한 값이 틀리면 리스크가 아니라 허위다
  9. lang 속성이 타입 스케일을 새게 했다 — 언어 전환 링크에 lang="en" 이 붙어 있고(스크린리더가 "English" 를 영어로 읽어야 하므로 옳다) [lang] 셀렉터가 스케일을 통째로 재정의해 그 링크만 16px 이 됐다. 접근성 표시가 조용히 디자인 토큰을 바꾸고 있었다. 크기는 헤더에서 확정하고 링크는 상속만 받게 고쳤다

측정 스크립트 자체의 오류도 한 번 있었다. 대비 계산에서 반투명 배경을 불투명으로 취급해 "English 1.15:1" 이라는 틀린 값이 나왔다. 알파를 합성하도록 고친 뒤 실패 0건. 검사 도구가 틀리면 통과도 실패도 의미가 없다.

최종 판정 (1440px · 320px 실렌더)

  • 하드 게이트 12개 전부 "아니오"
  • 대비: 본문·링크·버튼 전 요소 AA 이상, 실패 0건
  • 320px: 가로 스크롤 없음(문서 폭 305px), 뷰포트 초과 요소 0, 버튼 줄바꿈 0
  • 카운트: 섹션 9 / eyebrow 3(상한 ceil(9/3)=3) / 마퀴 0 / 히어로 헤드라인 2줄 / radius 어휘 2종 / 반투명 패널 2개
  • 슬롭 지문 grep: 0건

의도적으로 하지 않은 것과 그 이유

  • "Anti-Slop" 이라는 명명 — tasteskill 이 쓰는 말이다. 같은 단어를 쓰면 즉시 아류로 읽힌다
  • 경쟁 제품의 실측치를 페이지에 싣기 — 남의 수치는 반박 가능하다. 우리 수치만 쓴다
  • GitHub 스타 등 인기 지표 — 우리는 숫자가 0이다. 인기 대신 규율을 증거로 쓴다
  • 3D 위에 본문 얹기 — R2 가 준 원리의 핵심이다. 액자 위에 얹는 글자는 판독기 하나뿐이고, 그것도 바탕을 깔았다
  • GLB 모델 — 조사 결과 추상 지오메트리가 이 브리프에서 더 낫다. 모델 하나가 수 MB 이고, 우리가 파는 것은 모델이 아니라 절차다
  • 다크를 "멋있어서" 고르기 — 프리셋이 다크 기본값을 슬롭 시그니처 1위로 지목한다. 제품이 터미널이라는 근거로 고른다
  • 언어 자동 리다이렉트 / 국기 아이콘 — 사용자를 놀라게 하고, 언어는 국가가 아니다
  • 두 판을 번역으로 만들기 — 각 언어판을 따로 썼다

3D 의 역할 — 주인공이 아니라 재질

한 번 액자 안에 넣었다가 되돌렸다. R2(lusion.co)의 "3D 는 액자 안"은 3D 자체가 상품일 때의 규칙이다. 그 스튜디오는 3D 를 팔기 때문에 액자가 쇼케이스 프레임으로 정당하다. 우리는 절차를 파는데 액자 안에 넣을 상품이 없어서, 보는 사람이 "저 상자는 뭐냐"고 묻게 됐다. 게다가 의미를 게이트 01 / 06 판독기가 자막으로 설명하고 있었다 — 그림이 스스로 말하지 못해서 자막을 단 것이다.

이 씬의 역할은 재질이다. 공간을 만드는 표면이지 볼거리가 아니다. 그래서 규칙이 반대다:

주인공(액자) 재질(배경) — 현행
테두리 광 0.85 0.42
알파 0.72 0.34
UV 경계 smoothstep(0, 0.05) smoothstep(0, 0.17) + 제곱
카메라 이동 전 구간 60%
텍스트 액자 밖 위에 앉되 스크림으로 자리를 눌러줌

ResizeObserver 로 캔버스를 따라가게 했다. window.resize 만 듣던 동안 히어로 높이 변화를 놓쳐 배경이 화면의 3분의 2만 덮고 아래가 검게 비었다.

패럴랙스 지도 (실측)

깊이가 다른 층을 겹쳐 시차를 만든다. 한 층은 반드시 고정이다 — 전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.

자리 방식 실측 이동
히어로 배경 셰이더 흐름 필드 scroll(root) +56px
히어로 글자 scroll(root) 88px ↑ (반대 방향)
파이프라인 게이트 씬(three.js) sticky + 카메라 6단계를 통과
ㅤ└ 단계명 0 고정
ㅤ└ 산출물 view() ±14px
재료 · 설치 · 프리플라이트 배경 사진 fixed 0 — 원경
사례 배경 사진 drift 0.4 이미지 높이의 28.6%
조판 종이 drift 0.25 122px (근경)
재료 타일 view() ±26 / ±58px
프리플라이트 항목 view() 6 → 10px (뒤일수록 늦게)
수치 없음 의도적 정지

세 가지 깊이가 다 있다 — 원경(fixed) · 중경(drift) · 근경(콘텐츠와 함께). 종이를 원경으로 두지 않은 이유는 손에 닿는 거리의 재질이기 때문이다.

모바일(390px)에서도 fixed 가 유지된다. background-attachment: fixed 를 안 쓴 덕이다.

prefers-reduced-motion 에서는 게이트 씬이 아예 시작하지 않고, 고정 배경은 position: absolute 로 흐름에 돌아가며, 모든 view() 애니메이션이 꺼진다.

모션 문법 — 섹션마다 다른 말

같은 패럴랙스를 전부에 걸면 벽지다. 각 섹션의 주장을 움직임으로 옮겼다.

섹션 주장 움직임
히어로 결정하지 않으면 기본값이 나온다 글자 ↑ / 배경 ↓ — 반대 방향이라야 시차가 보인다
파이프라인 단계를 밟으면 결과가 나온다 단계명 고정, 산출물만 지연
사례 같은 스킬에서 다른 결과 두 판이 다른 속도
재료 표면은 한 겹이 아니다 타일이 서로 다른 깊이
프리플라이트 시간순 기록 뒤 항목일수록 늦게
수치 공개한 값은 검증 가능하다 없음 — 의도적 정지

마지막 줄이 결정이다. 숫자가 스크롤에 흔들리면 그 값도 흔들려 보인다.

배경 레이어 — 패럴랙스의 기준면

배경이 단색이면 패럴랙스는 성립하지 않는다. 앞의 것이 몇 px 어긋나도 비교할 대상이 없다. 히어로 말고는 기준면이 없어서 시차가 안 보였다.

배경 이미지를 사 오는 대신 이 페이지가 이미 주장하는 것을 그렸다.

레이어 무엇 어디 근거
기준면은 BgPhoto 컴포넌트가 만든다(아래 표).

CSS 격자를 한동안 같이 깔았다가 걷어냈다. 사진이 들어온 뒤로 배경이 두 겹이 되어 서로 경쟁했고, 격자는 배경이라기보다 로 읽혔다. 배경도 한 섹션에 하나다.

한 층은 반드시 고정이다 — 파이프라인의 단계명은 움직이지 않는다. 전부 움직이면 기준이 없어 그냥 흔들리는 페이지가 된다.

backdrop-filter 오판을 정정했다

"backdrop-filter 에 SVG 를 얹으면 Chrome 이 선언 전체를 버린다" 고 적어두고 페이지의 프리플라이트 로그에까지 실었다. 사실이 아니었다.

Chrome 151 에서 재측정: computed 값이 none 이 아니라 url("#id") 로 유지되고, 대비 높은 배경 위에서 실제로 굴절된다. 진짜 원인은 feImagewidth/height 를 요소 크기와 맞추지 않은 것이었다 — backdrop-filter 는 필터 이미지를 자동으로 조정하지 않는다.

아직 헤더에 얹지 않은 이유는 브라우저 탓이 아니라 비용이다. Chrome 전용이고, 요소가 리사이즈될 때마다 변위 맵을 다시 만들어야 한다. 헤더는 폭이 계속 변한다.

한 번 실패한 것을 '안 되는 것'으로 적으면 그 문장이 다음 사람의 상한선이 된다. 로그 항목을 그 내용으로 바꿔 실었다.

패럴랙스를 "고정"으로 바꿨다

세 번 고친 뒤에도 티가 나지 않았다. 원인은 방식 자체였다 — 느리게 같이 움직이는 것으로는 원경이 안 된다. 속도만 다를 뿐 방향이 같기 때문이다.

배경을 뷰포트에 고정한다. background-attachment: fixed 는 iOS Safari 가 throttle 하므로 쓰지 않고, 섹션에 clip-path: inset(0) 을 걸어 position: fixed 의 컨테이닝 블록으로 만든다. 그러면 배경이 화면에 멈춘 채 그 섹션 안에서만 보인다.

실측: 스크롤 1000px 구간에서 배경의 viewportTop 이 계속 0.

대가가 둘 있고 둘 다 받아들였다.

  • 조상에 transform·filter·mask 를 못 쓴다. 그것들도 컨테이닝 블록을 만들어 고정을 깨뜨린다. 그래서 위아래 페이드 마스크를 포기하고 경계는 clip 으로 딱 자른다
  • prefers-reduced-motion 에서는 position: absolute 로 되돌린다

전부 고정하지는 않았다. 원경과 중경을 나눴다.

섹션 모드
재료 · 설치 · 프리플라이트 fixed 원경. 콘텐츠만 그 위를 지나간다
사례 drift 0.4 두 결과물이 흐르는 자리라 배경도 같이 흐른다
조판 drift 0.25 종이는 손에 닿는 거리다. 원경으로 두면 거짓말이 된다

유리 테두리는 conic 이 아니라 linear 로 돈다

헤더의 헤어라인이 변마다 끊겨 보였다. conic-gradient 로 만들었기 때문이다. conic 은 중심에서 각도로 도는데, 1180×54 처럼 납작한 형태에서는 각도가 급변해서 긴 변에서 색이 거의 안 변하고 짧은 변에서 한꺼번에 바뀐다. 실제로 아래쪽만 절반 밝았다.

linear-gradient(100deg, …) 로 바꾸면 네 변이 같은 그라디언트를 공유해 테두리를 따라 이어진다. 마스크로 1px 만 남기는 구조는 그대로다.

그리고 아주 얕은 글로우를 얹었다 — 0 0 22px -8px 에 강조색을 42% 섞었다. 유리가 차갑게만 보이지 않게 하는 정도이고, 밝은 밴드에서는 흰빛 대신 그림자로 뒤집는다(--glass-rim·--glass-bloom.band-inverse 에서 재정의).

테두리 광을 회전시켰다

정지한 헤어라인 위에 도는 빛을 얹었다. conic 의 각도를 @property 로 돌리면 매 프레임 배경을 다시 칠하므로(하드 게이트 #8), 정사각형 원뿔 레이어를 마스크 안에서 rotate 시킨다. 움직이는 것은 transform 뿐이다.

aspect-ratio: 1 로 정사각형을 유지해야 회전 중 밝기가 일정하고, width: 200% 로 크게 잡아야 1178×52 짜리 헤더에서도 모서리까지 닿는다. 주기 11초.

three.js 모바일 대응

세로 화면에서 게이트가 통째로 안 보이고 있었다. 수직 시야는 화면 비율과 무관하지만 수평 시야는 aspect 에 비례한다 — 데스크톱(2.5:1)에서 정한 판 폭 15 가 세로 화면(0.45:1)의 수평 시야 2.5 에 비하면 여섯 배라, 첫 판이 화면을 완전히 덮었다.

fit() 에서 camera.aspect 를 갱신할 때 판의 scale.x 도 같이 잡는다.

그리고 저사양 판정을 고쳤다. hardwareConcurrency <= 4 만 보면 요즘 폰이 전부 고사양으로 잡힌다(8코어가 흔하다). innerWidth < 760 을 함께 본다 — 좁은 화면은 대개 배터리로 돌고 열이 더 난다. DPR 도 1.5 → 1.25, 캔버스 불투명도도 0.7 → 0.45(세로에서는 화면 전체를 덮으므로).

three.js 씬 셋은 서로 다른 것이어야 한다

게이트 씬을 파이프라인으로 옮기면서 히어로에도 그대로 남겨뒀더니 같은 그림이 페이지에 두 번 나왔다. 히어로를 다른 것으로 바꿨다.

자리 무엇
히어로 shader-plane 흐름 필드 노이즈를 두 번 접어 빛이 유리를 지난 결을 만든다. 공간을 만드는 재질
파이프라인 gate-scene 유리판 여섯 장 스크롤하면 통과하고 그 단계가 켜진다. 의미를 가진 주인공
재료 shader-plane 스테이지 SVG 필터 타일이 앉는 무대

히어로에는 무거운 씬을 임포트하지 않는다 — three.md §0 이 권하는 대로 셰이더 플레인 하나다. 파일 이름도 hero-scene.tsgate-scene.ts 로 고쳤다. 이름이 실제 용도와 다르면 다음 사람이 히어로 것인 줄 알고 고친다.

캔버스 배경 위 본문 대비 실측(스크린샷 픽셀): 최악 8.46:1(가장 밝은 중앙 여백), h1 자리 14.83:1.

readPixels 로 재려다 전부 0 이 나왔다. preserveDrawingBuffer: false 면 버퍼가 비워져 있다. WebGL 위 대비는 스크린샷 픽셀로 재라.

게이트 씬을 파이프라인으로 옮겼다

히어로에서 뺐던 유리판 여섯 장을 여기로 되살렸다. 같은 씬인데 자막이 필요 없어졌다.

히어로에 뒀을 때 파이프라인으로 옮긴 뒤
옆에 있는 것 헤드라인과 CTA 여섯 단계의 실제 이름
의미 전달 게이트 01 / 06 판독기 — 자막 판을 통과하면 그 단계가 켜진다

캔버스는 position: sticky; height: 100dvh 로 화면에 머물고 섹션이 지나간다. 진행도는 섹션 높이가 아니라 (섹션 높이 뷰포트 높이)로 잰다. 섹션 높이로 나눴더니 캔버스가 사라진 뒤에도 진행이 남아 6단계 중 3단계에서 멈췄다. 그리고 기준 요소는 캔버스가 아니라 섹션이다 — sticky 캔버스는 자기 rect 가 변하지 않는다.

캔버스 위 텍스트 대비 실측 14.2:1(캔버스 불투명도 0.7).

패럴랙스가 세 번 실패한 경위

1. 이동량이 모자랐다. 여유를 28% 잡아놓고 ±46px(총 92px)만 움직였다. 스크롤 1800px 구간에서 이동 비율 5% — 아무도 시차를 느끼지 못했다. 공식은 이미지를 컨테이너보다 크게 만들고 그 초과분 전체를 움직이는 것이다. depth 0.45 → 이미지 145%, 이동 translateY(-31.03%). 같은 구간에서 374px.

2. overflow: hiddenview() 타임라인을 죽이고 있었다. overflow: hidden 은 스크롤 컨테이너를 만들고, 그러면 자식의 view() 가 문서가 아니라 그 상자를 기준으로 잡힌다. 이미지는 상자 안에서 늘 같은 자리라 진행도가 고정된다. 콘솔 에러도 없고 computed 값도 정상이라 두 스크롤 위치에서 transform 을 재보고서야 이동 0px 인 것을 알았다. 잘라내는 일은 섹션의 overflow: clip 에 맡겼다 — clip 은 스크롤 컨테이너를 만들지 않는다.

3. opacity 로 눌러 이미지를 지웠다. 어두운 광원 사진을 opacity: 0.5 로 깔면 검정도 빛도 같이 죽어 있는지조차 보이지 않는다. mix-blend-mode: screen 으로 바꿨다 — 검정은 투명해지고 밝은 곳만 더해지므로 불투명도를 0.85~1.0 으로 유지해도 본문 대비가 안전하다. 밝은 종이만 normal 이다.

생성 프롬프트에도 같은 함정이 있었다. "본문 뒤에 깔리니 어둡게"를 너무 강하게 넣어서 안전한 이미지가 아니라 없는 이미지를 만들었다. 대비는 합성 방식으로 풀 문제였다.

배경 사진 넉 장 (gpt-image-2 로 생성)

CSS 그라디언트 광원만으로는 기준면이 약해서, 굴절과 그레인이 실제로 들어 있는 이미지를 넣었다. Codex CLI 의 내장 image_gen(gpt-image-2)을 ChatGPT 구독 인증으로 호출해 만들었다 — 스톡 이미지가 아니다.

섹션마다 다른 그림을 쓴다. 같은 배경을 전부에 깔면 그건 벽지고, 모션에 적용한 규칙("섹션마다 다른 말을 해야 한다")이 배경에서만 면제될 이유가 없다.

이미지 무엇 어디 그 섹션의 주장
bg-refraction 겹친 유리판을 통과한 빛 재료 · 설치(flip) 표면은 한 겹이 아니다
bg-split 한 빔이 둘로 갈라진다 사례 같은 파이프라인, 다른 결과
bg-align 정렬선 여럿 중 하나가 어긋나 끊겨 있다 프리플라이트 자기 결과물을 남의 것처럼 본다
bg-paper 무광 인쇄용지, 섬유와 데클 엣지 조판(밝은 밴드) 한글이 종이 위에 있어야 한다

격자를 쓰는 파이프라인·수치에는 사진을 넣지 않았다. 거기서는 격자가 그 섹션의 주장이다. 규칙·히어로도 그대로 뒀다 — 히어로에는 three.js 가 있고, 규칙 목록은 담백해야 한다.

프롬프트는 이 페이지의 토큰 값을 그대로 넣었다:

  • 바탕 #0B0E12 — 순흑도 네이비도 아니게
  • 광원 둘만: 좌상단 딥 틸 #4FD6C4, 우하단 채도 낮은 파랑 #3D7BD6. 보라·마젠타·바이올렛·무지개·따뜻한 색 금지 (슬롭 지문)
  • 프로스티드 글래스 판이 거의 옆에서 보이게 겹쳐, 가장자리만 얇게 빛나고 가운데는 거의 안 보이게
  • 대비를 아주 낮고 고르게 — "이 이미지는 흰 본문 텍스트 뒤에 깔린다. 활자와 경쟁할 만큼 밝은 곳이 있으면 안 된다. 중앙 띠는 조용하고 어둡게, 밝기는 모서리 쪽에"
  • 미세 필름 그레인, 밴딩 없이
  • 금지: 사물·로고·글자·숫자·사람·렌즈 플레어·보케·딱딱한 도형·비네트

components/BgPhoto.astro 로 감쌌다. CSS background-image 가 아니라 <Image loading="lazy"> 를 절대배치한다 — background-image 로 깔면 항상 받고 최적화도 안 된다. 같은 이미지를 설치 섹션에서는 flip 으로 좌우 반전해 같은 그림으로 읽히지 않게 했다.

소스 WebP: refraction 37KB · split 14KB · align 11KB · paper 112KB (종이만 큰 이유는 섬유 질감이 고주파라 압축이 안 붙기 때문이다)

빌드 결과 12개 파일 합계 276KB. 실제로는 뷰포트에 맞는 크기 하나씩만, 그것도 loading="lazy" 로 받는다.

절대배치 배경이 그리드 안에서 561px 로 잘렸다. .section 은 subgrid 이고 .section > * { grid-column: main; } 이 걸려 있는데, 절대배치 요소라도 그리드 영역이 잡히면 그 영역이 컨테이닝 블록이 된다. 그래서 배경이 화면 가운데 561px 띠로만 깔렸고, overflow: visible 이라 위아래 14% 여유분이 이웃 섹션 위로 넘쳐 조판 섹션의 종이가 프리플라이트 하단에 밝은 띠로 나타났다. .bg-photo { grid-column: full } + .has-bg { overflow: clip } 으로 고쳤다. position: absolute 는 그리드에서 배치를 면제해주지 않는다.

codex 0.147.0 메모codex-image 스킬의 extract_imagegen.py 는 rollout JSONL 의 image_generation_call base64 를 찾는데, 0.147.0 은 다시 파일로 저장한다 (~/.codex/generated_images/<sid>/exec-*.png, 이벤트는 custom_tool_call + image_generation_end). 그래서 추출이 fail-loud 로 죽는다 — 이미지는 실제로 생성돼 있다. 그리고 $imagegen프롬프트 맨 앞에 있어야 내장 도구가 발동한다.

그리드의 wide 트랙을 좁은 화면에서 닫는다

--wide-pad: clamp(0rem, -3.7rem + 15.2vw, 10rem)

고정 10rem 으로 두는 동안 모바일에서도 좌우 80px 씩 살아남아 390px 화면에서 본문이 x=112 부터 시작하고 폭이 278px 로 줄었다. 파이프라인 제목이 네 줄로 접혔다. 닫고 나서 모든 텍스트가 x=32 에서 시작하고 제목이 두 줄이 됐다.

같은 이유로 파이프라인의 제목·리드·브리프를 스파인과 한 컨테이너에 넣었다. 섹션 기본 그리드에 두면 제목 x=371 · 리드 x=435 · 스파인 x=143 으로 정렬선이 셋이 된다. 지금은 전부 x=183 이다.

디자인 SSOT

토큰이 없어서 깨져 있던 세 축을 채웠다.

  • 웨이트 3종400 / 500 / 600. 이전에는 코드 곳곳에 500·560·600·620·660 이 흩어져 6종이었고 셋은 토큰에 없는 값이었다. R1 모바일 실측은 400 이 832회, 나머지 12회다
  • 컨트롤 치수--control-h: 44px · --control-h-sm: 36px · --control-pad-x. 버튼처럼 생긴 것은 전부 .btn + .btn-solid / .btn-quiet 를 쓴다. 이전에는 두 버튼이 높이 36 vs 44, 패딩 16 vs 8, 테두리 1px vs 0, 웨이트 400 vs 500 이었다
  • --header-h — 떠 있는 헤더의 높이. 간격 토큰을 clamp() 로 바꾸는 순간 관계가 끊겨 h1 이 헤더 뒤로 들어갔다. 값이 아니라 관계를 토큰으로 둔다
  • 반응형 간격--space-5~8--gutterclamp() 로. 고정이면 모바일에서 섹션 상하 112px 이 그대로 들어가 화면의 26% 를 여백이 먹는다
  • 스크롤바scrollbar-color 지정. 기본값이면 어두운 페이지 옆에 OS 색 밝은 막대가 선다

모바일

  • 좌우 여백 24px → 32px (R1 모바일 실측값)
  • 섹션 상하 112px → 56px
  • 헤더에 섹션 앵커 4개 추가. 900px 미만에서는 <details> 기반 메뉴로 접힌다 — JS 0바이트이고 열림 상태·Esc·포커스 순서·aria-expanded 를 브라우저가 맞게 한다
  • 390px 실렌더: 가로 스크롤 0, 대비 실패 0, 웨이트 3종, radius 2종

히어로 3D 의 형태 결정

  • 15 × 6 (액자 비율 2.5:1 과 일치). 첫 판이 화면을 완전히 덮어 이미 첫 게이트 안에 들어와 있다
  • 판이 화면보다 좁으면 통과하는 내내 좌우 테두리가 남아 세로 기둥 두 개로 보인다. 실제로 그렇게 보였다
  • 판 높이가 시야보다 작으면 위아래 테두리가 액자 경계에 겹쳐 이중선이 된다
  • 어긋남(sway)은 0.16 + 0.05i 로 아주 작게. 키우면 '통과하는 문'이 아니라 쌓아둔 사각형이 된다. 중심축이 살아야 원근이 터널로 읽힌다
  • GATE_GAP 6.4, 카메라 시작 z 7.2

다음 실행이 해야 할 일

  1. 배포 후 Lighthouse 로 첫 인터랙션·LCP·CLS 를 재고 "미측정" 을 채운다. 채우기 전까지 비워둔다
  2. 폰트를 CDN 의존에서 자체 호스팅으로 옮길지 판단한다 (현재 jsDelivr · Fontshare · Google Fonts 3곳)
  3. OG 이미지가 없다
  4. 섹션이 9개, 총 높이 11,197px 이다. 스크롤 보상이 충분한지 재검토할 여지가 있다

2026-08-20 · 2판. 0~6단계 전체 경로 재주행.