refactor(site): 섹션 순서를 성격별로 묶고 분량을 우선순위에 맞춘다
Some checks are pending
ci / build (push) Waiting to run

지적마다 그 섹션만 키운 결과 15.5 화면이 됐고, 기능 설명과 증거가 번갈아 나와
지금 무슨 이야기를 듣고 있는지 알 수 없었다.

순서를 네 묶음으로 정리했다.
  ① 무엇인가      히어로 · 파이프라인
  ② 무엇을 아는가  SVG 필터 · 한글 조판
  ③ 정말 그런가    사례 · 수치 · 자기 감사 · 규칙
  ④ 행동          설치

분량:
- SVG 필터 3.1 → 2.1 화면. 스킬 문서에 있을 구현 디테일을 페이지에 옮겨 적고
  있었다. 데모는 넷 다 두고 설명만 줄였다.
- .stage 의 padding 에 --wide-pad 를 쓰고 있었다. 이 토큰은 페이지 그리드의
  바깥 칼럼용(최대 10rem)이라 1440px 에서 폭 320px 이 사라졌고, 그만큼 설명
  칸이 170px 로 눌려 여덟 자마다 줄이 바뀌었다.
- 자기 감사 항목 13개 → 6개. 나열이 밀도를 대신하고 있었다.

파이프라인 끝의 마무리 문장을 삭제했다. sticky 게이트가 끝까지 돌 스크롤
구간이 필요해서 만든 자리에 문장을 채운 것이라, 읽는 사람에게는 앞뒤 없이
튀어나온 문장이었다. 여백이 필요하면 여백을 만든다.

전체 15.5 → 13.5 화면.
This commit is contained in:
Yun Chan 2026-08-21 00:34:40 +09:00
parent 9a09678dee
commit 3271a329af
5 changed files with 79 additions and 73 deletions

View file

@ -50,7 +50,6 @@ const MAP_Y = map(true, "rgb(128,255,128)", "rgb(128,0,128)");
c.demos.map((d, i) => ( c.demos.map((d, i) => (
<li class="fdemo reveal" style={`--reveal-delay:${i * 70}ms`}> <li class="fdemo reveal" style={`--reveal-delay:${i * 70}ms`}>
<h3 class="fd-name">{d.name}</h3> <h3 class="fd-name">{d.name}</h3>
<p class="fd-problem">{d.problem}</p>
<div class="fd-pair"> <div class="fd-pair">
{[false, true].map((on) => ( {[false, true].map((on) => (
@ -63,8 +62,11 @@ const MAP_Y = map(true, "rgb(128,255,128)", "rgb(128,0,128)");
))} ))}
</div> </div>
<div class="fd-text">
<p class="fd-problem">{d.problem}</p>
<p class="fd-how muted">{d.how}</p> <p class="fd-how muted">{d.how}</p>
<p class="fd-verdict mono">{d.verdict}</p> <p class="fd-verdict mono">{d.verdict}</p>
</div>
</li> </li>
)) ))
} }
@ -165,7 +167,10 @@ const MAP_Y = map(true, "rgb(128,255,128)", "rgb(128,0,128)");
position: relative; position: relative;
max-width: 1180px; max-width: 1180px;
margin-inline: auto; margin-inline: auto;
padding: var(--wide-pad); /* --wide-pad 는 페이지 그리드의 바깥 칼럼을 정의하는 토큰이다(최대 10rem).
여기에 그대로 쓰면 1440px 화면에서 좌우 160px 씩, 폭 320px 이 사라진다.
그만큼 오른쪽 설명 칸이 170px 로 줄어 여덟 자마다 줄이 바뀌었다. */
padding: var(--space-6);
border-radius: var(--radius-frame); border-radius: var(--radius-frame);
overflow: hidden; overflow: hidden;
isolation: isolate; isolation: isolate;
@ -197,14 +202,26 @@ const MAP_Y = map(true, "rgb(128,255,128)", "rgb(128,0,128)");
} }
.fdemo + .fdemo { border-top: 1px solid var(--line); padding-top: var(--space-6); } .fdemo + .fdemo { border-top: 1px solid var(--line); padding-top: var(--space-6); }
.fd-name { font-size: var(--step-1); } /* 데모와 설명을 세로로 쌓으면 한 항목이 320px 을 먹는다.
.fd-problem { max-width: 52ch; color: var(--ink-muted); } 넷이면 섹션이 3.1 화면 — 파이프라인 설명보다 길어졌다.
그림은 왼쪽, 말은 오른쪽에 두면 같은 내용이 절반 높이에 들어간다. */
.fdemo {
display: grid;
grid-template-columns: minmax(0, 584px) minmax(0, 1fr);
column-gap: var(--space-6);
align-items: start;
}
.fd-name {
grid-column: 1 / -1;
font-size: var(--step-1);
margin-bottom: var(--space-4);
}
.fd-problem { color: var(--ink-muted); }
.fd-pair { .fd-pair {
display: grid; display: grid;
grid-template-columns: repeat(2, minmax(0, 300px)); grid-template-columns: repeat(2, minmax(0, 280px));
gap: var(--space-4); gap: var(--space-3);
margin-block: var(--space-4);
} }
.fd-side { margin: 0; } .fd-side { margin: 0; }
.fd-tag { .fd-tag {
@ -215,13 +232,16 @@ const MAP_Y = map(true, "rgb(128,255,128)", "rgb(128,0,128)");
.is-bad { color: var(--ink-muted); } .is-bad { color: var(--ink-muted); }
.is-good { color: var(--accent); } .is-good { color: var(--accent); }
.fd-how { max-width: 60ch; } .fd-text > * + * { margin-top: var(--space-3); }
.fd-verdict { .fd-verdict {
margin: var(--space-2) 0 0; margin: var(--space-2) 0 0;
font-size: var(--mono-size); font-size: var(--mono-size);
color: var(--accent); color: var(--accent);
} }
@media (max-width: 1080px) {
.fdemo { grid-template-columns: 1fr; row-gap: var(--space-4); }
}
@media (max-width: 720px) { @media (max-width: 720px) {
.fd-pair { grid-template-columns: 1fr; } .fd-pair { grid-template-columns: 1fr; }
} }

View file

@ -49,7 +49,6 @@ const { c } = Astro.props;
)) ))
} }
</ol> </ol>
<p class="after muted reveal">{c.after}</p>
</div> </div>
</section> </section>
@ -169,7 +168,7 @@ const { c } = Astro.props;
space-7 까지 올렸더니 이번엔 정거장 사이가 224px 로 벌어져 space-7 까지 올렸더니 이번엔 정거장 사이가 224px 로 벌어져
목록이 흩어져 보였다 — 여섯 개가 한 목록으로 안 읽힌다. 목록이 흩어져 보였다 — 여섯 개가 한 목록으로 안 읽힌다.
읽기 간격과 스크롤 구간은 같은 손잡이가 아니다. 읽기 간격과 스크롤 구간은 같은 손잡이가 아니다.
간격은 읽기에 맞추고, 스크롤 구간은 스파인 뒤 여백(.after)에서 번다. */ 간격은 읽기에 맞추고, 스크롤 구간은 스파인 뒤 여백에서 번다. */
padding-block: var(--stop-pad); padding-block: var(--stop-pad);
display: grid; display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr); grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
@ -226,13 +225,11 @@ const { c } = Astro.props;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
.after { /* sticky 게이트가 끝까지 돌려면 스파인 뒤에 스크롤 구간이 필요하다.
margin-top: var(--space-7); 한동안 여기에 마무리 문장을 한 줄 놓아 그 자리를 채웠는데,
max-width: var(--measure); 읽는 사람에게는 뜬금없는 문장이었다 — 여백이 필요하면 여백을 만든다.
/* 여기가 스크롤 구간을 버는 자리다. 정거장 간격을 벌리는 대신 문장으로 여백을 대신하지 않는다. */
마지막 정거장 뒤에 여운을 둔다 — 스파인이 끝난 뒤라 빈 공간으로 읽히지 않는다 */ .spine { padding-bottom: 62dvh; }
padding-bottom: 62dvh;
}
@media (max-width: 780px) { @media (max-width: 780px) {
.stop { .stop {

View file

@ -49,7 +49,6 @@ export interface Content {
briefLabel: string; briefLabel: string;
brief: string; brief: string;
stages: Stage[]; stages: Stage[];
after: string;
}; };
showcase: { showcase: {
h2: string; h2: string;
@ -208,8 +207,6 @@ export const ko: Content = {
briefLabel: "들어온 브리프", briefLabel: "들어온 브리프",
brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘", brief: "수제 커피 로스터리의 원두 정기구독 랜딩 만들어줘",
stages: STAGES_KO, stages: STAGES_KO,
after:
"여섯 단계 끝에 남는 것은 페이지 하나와 design.md 하나다. 다음 사람이 그 파일을 읽고 같은 결정을 이어간다.",
}, },
showcase: { showcase: {
h2: "결과물은 서로 닮지 않는다", h2: "결과물은 서로 닮지 않는다",
@ -237,38 +234,38 @@ export const ko: Content = {
{ {
id: "scrim", id: "scrim",
name: "글자 모양대로 생기는 스크림", name: "글자 모양대로 생기는 스크림",
problem: "사진 위 캡션이 대비 4.5:1 을 못 넘는다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞춰야 한다.", problem: "사진 위 캡션이 대비를 못 넘긴다. 사진마다 밝기가 달라 반투명 박스를 손으로 맞추게 된다.",
beforeLabel: "그냥 얹음", beforeLabel: "그냥 얹음",
afterLabel: "filter: url(#scrim)", afterLabel: "filter: url(#scrim)",
how: "feMorphology 로 글자 알파를 부풀리고 블러한 뒤, 그 모양대로만 feFlood 를 채운다. 박스가 아니라 글자를 따라가므로 사진을 가리지 않는다.", how: "글자 알파를 부풀려 그 모양대로만 색을 채운다. 박스가 아니라 글자를 따라가므로 사진을 덜 가린다.",
verdict: "흰 글자 대비 1.70:1 → 4.79:1 · 사진을 바꿔도 다시 맞출 것이 없다", verdict: "흰 글자 대비 1.70:1 → 4.79:1",
}, },
{ {
id: "cvd", id: "cvd",
name: "색각 이상 시뮬레이션", name: "색각 이상 시뮬레이션",
problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 된다. 눈으로는 절대 못 잡는다.", problem: "상태를 색으로만 구분하면 남성 12명 중 1명에게 같은 배지가 된다. 내 화면만 봐서는 못 잡는다.",
beforeLabel: "내가 보는 화면", beforeLabel: "내가 보는 화면",
afterLabel: "적록색약(2형)", afterLabel: "적록색약(2형)",
how: "feColorMatrix 하나다. 이것만은 sRGB 로 두면 안 된다 — 색각 변환은 물리적 혼합이라 linearRGB 가 맞다. 필터 전체에서 유일한 예외다.", how: "feColorMatrix 한 줄. 이것만은 linearRGB 로 둔다 — 색각 변환은 물리적인 빛의 혼합이다.",
verdict: "배지의 최소 색거리 82 → 2 · 주의와 실패가 같은 색이 된다", verdict: "배지의 최소 색거리 82 → 2 · 주의와 실패가 같은 색",
}, },
{ {
id: "goo", id: "goo",
name: "떨어진 것이 하나로 뭉치기", name: "떨어진 것이 하나로 뭉치기",
problem: "조각이 서로 녹아 붙는 표현을 하려고 캔버스나 물리 라이브러리를 켠다. 로더, 메뉴 병합, 인디케이터 전환에서 반복된다.", problem: "조각이 녹아 붙는 표현에 캔버스나 물리 라이브러리를 켠다. 로더와 메뉴 전환에서 반복된다.",
beforeLabel: "필터 없음 · 원 3개", beforeLabel: "필터 없음 · 원 3개",
afterLabel: "filter: url(#goo)", afterLabel: "filter: url(#goo)",
how: "크게 블러한 뒤 feColorMatrix 로 알파만 18배 세워 잘라낸다. 붙을지 말지는 눈대중이 아니라 산수다 — 두 원 사이 중간점의 블러 알파가 문턱(7÷18 ≒ 0.39)을 넘어야 한다. 간격 18px 에 블러 7 로는 0.2 근처라 못 넘었고, 12px 에 10 으로 넘겼다.", how: "블러한 뒤 알파만 세워 잘라낸다. 붙을지 말지는 취향이 아니라 간격과 블러 반경의 비율로 정해진다.",
verdict: "가로로 세어 덩어리 3개 → 1개 · JS 0줄", verdict: "덩어리 3개 → 1개 · JS 0줄",
}, },
{ {
id: "glass", id: "glass",
name: "배경을 휘게 하는 오버레이", name: "배경을 휘게 하는 오버레이",
problem: "반투명 패널은 뒤를 흐리게만 만든다. 실제 유리는 빛을 휜다 — 그 차이가 패널을 화면 위가 아니라 화면 안에 놓는다.", problem: "반투명 패널은 뒤를 흐리게만 만든다. 실제 유리는 빛을 휜다.",
beforeLabel: "backdrop-filter: blur", beforeLabel: "backdrop-filter: blur",
afterLabel: "blur + url(#lens)", afterLabel: "blur + url(#lens)",
how: "변위 지도를 backdrop-filter 에 넘긴다. R 이 X, G 가 Y 이동량이고 128 이 중립이다. 지도를 feTurbulence 로 만들면 값이 128 로 수렴해 아무 일도 일어나지 않는다 — 가장자리에서 안쪽으로 미는 그라디언트를 직접 그려야 한다.", how: "변위 지도를 backdrop-filter 에 넘긴다. 노이즈로 만들면 값이 중립으로 수렴해 아무 일도 안 일어난다.",
verdict: "가장자리에서 격자가 눌린다 · 지도 크기가 요소와 다르면 조용히 아무 일도 안 일어난다 · Chromium 전용", verdict: "가장자리에서 격자가 눌린다 · Chromium 전용",
}, },
], ],
}, },
@ -296,6 +293,7 @@ export const ko: Content = {
h2: "이 페이지를 만들며 실제로 걸린 것", h2: "이 페이지를 만들며 실제로 걸린 것",
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.", lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
items: [ items: [
{ {
caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었다", caught: "폰트가 교체되는 순간 글자 폭이 13% 튀고 있었다",
fixed: fixed:
@ -326,39 +324,6 @@ export const ko: Content = {
fixed: fixed:
"밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.", "밴드가 강조색만 반전시키고 그 위 글자색은 흰색 그대로였다. 반전 대응 토큰을 하나 더 두고 함께 바꿨다.",
}, },
{
caught: "320px 에서 가로로 13px 넘쳤다. 표를 스크롤 컨테이너로 감쌌는데도",
fixed:
"grid item 의 기본 min-width 는 auto 라 콘텐츠가 부모를 밀어낸다. min-width:0 을 걸어야 overflow 가 실제로 작동한다.",
},
{
caught: "eyebrow 라벨이 6개. 카운트 규칙 상한은 3개",
fixed: "세 섹션만 남기고 나머지는 헤드라인만으로 구분했다. 라벨이 없어도 섹션은 읽힌다.",
},
{
caught: "본문 컬럼이 960px. 우리가 정한 한 줄 길이는 561px",
fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.",
},
{
caught: "방향을 바꾸면서 이 스킬의 1단계를 건너뛰었다",
fixed:
"재질을 유리로 바꾸라는 요청은 2단계 방향 재결정이고, 그때는 전체 경로로 올려 레퍼런스부터 다시 봐야 한다. 그러지 않고 구현으로 바로 들어갔다. 결과는 이전 방향의 뼈대에 유리를 바른 페이지였고 design.md 는 옛 결정을 그대로 담고 있었다. 0단계부터 다시 돌았다.",
},
{
caught: "유리판이 정면으로 서 있어 프레넬이 구조적으로 0이었다",
fixed:
"평면이 카메라를 정면으로 보면 법선과 시선의 내적이 1이라 가장자리 반사가 계산되지 않는다. 유리로 읽히게 하는 유일한 신호가 죽어 있었고, 그래서 판 여섯 장이 흐린 사각형으로 보였다. 법선을 미세하게 굽히고 테두리 광을 UV 경계에서 따로 만들었다.",
},
{
caught: "페이지의 본체인 파이프라인이 화면의 43%만 쓰고 있었다",
fixed:
"1440px 중 625px. 아홉 섹션에서 가장 좁으면서 높이는 1637px 로 가장 길었다. 단계 설명과 산출물을 두 열로 열어 1113px 로 넓혔다.",
},
{
caught: "대비를 재는 스크립트가 반투명 배경을 불투명으로 계산했다",
fixed:
"\"English 1.15:1\" 이라는 틀린 값이 나왔다. 알파를 아래에서 위로 합성하도록 고치자 실패 0건이었다. 검사 도구가 틀리면 통과도 실패도 의미가 없다.",
},
], ],
note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못한다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.", note: "열세 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 사람이 잡은 것이 셋이다. 카드가 전형적이라는 말, 폰트를 제대로 다루지 못한다는 말, 그리고 우리 스킬을 진짜로 쓴 게 맞냐는 말.",
}, },
@ -472,8 +437,6 @@ export const en: Content = {
briefLabel: "The brief that came in", briefLabel: "The brief that came in",
brief: "Build a subscription landing page for a small-batch coffee roastery", brief: "Build a subscription landing page for a small-batch coffee roastery",
stages: STAGES_EN, stages: STAGES_EN,
after:
"What remains after six stages is one page and one design.md. The next person reads that file and continues the same decisions.",
}, },
showcase: { showcase: {
h2: "The outputs do not resemble each other", h2: "The outputs do not resemble each other",

View file

@ -13,14 +13,27 @@ import { en as c } from "../../i18n/content";
--- ---
<Base lang="en" title={c.meta.title} description={c.meta.description}> <Base lang="en" title={c.meta.title} description={c.meta.description}>
{/*
순서는 세 묶음이다. 한동안 기능과 증거가 번갈아 나와서
(사례 → 필터 → 수치 → 감사 → 조판 → 규칙) 읽는 사람이 지금 무슨 이야기를
듣고 있는지 놓쳤다. 같은 성격끼리 붙인다.
① 무엇인가 히어로 · 파이프라인
② 무엇을 아는가 SVG 필터 · 한글 조판
③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙
④ 행동 설치
*/}
<Hero c={c.hero} ui={c.ui} /> <Hero c={c.hero} ui={c.ui} />
<Pipeline c={c.pipeline} /> <Pipeline c={c.pipeline} />
<Showcase c={c.showcase} />
<Materials c={c.materials} /> <Materials c={c.materials} />
<Typography c={c.typography} />
<Showcase c={c.showcase} />
<Metrics c={c.metrics} /> <Metrics c={c.metrics} />
<Preflight c={c.preflight} /> <Preflight c={c.preflight} />
<Typography c={c.typography} />
<Rules c={c.rules} /> <Rules c={c.rules} />
<Install c={c.install} ui={c.ui} /> <Install c={c.install} ui={c.ui} />
</Base> </Base>

View file

@ -13,14 +13,27 @@ import { ko as c } from "../i18n/content";
--- ---
<Base lang="ko" title={c.meta.title} description={c.meta.description}> <Base lang="ko" title={c.meta.title} description={c.meta.description}>
{/*
순서는 세 묶음이다. 한동안 기능과 증거가 번갈아 나와서
(사례 → 필터 → 수치 → 감사 → 조판 → 규칙) 읽는 사람이 지금 무슨 이야기를
듣고 있는지 놓쳤다. 같은 성격끼리 붙인다.
① 무엇인가 히어로 · 파이프라인
② 무엇을 아는가 SVG 필터 · 한글 조판
③ 정말 그런가 사례 · 수치 · 자기 감사 · 규칙
④ 행동 설치
*/}
<Hero c={c.hero} ui={c.ui} /> <Hero c={c.hero} ui={c.ui} />
<Pipeline c={c.pipeline} /> <Pipeline c={c.pipeline} />
<Showcase c={c.showcase} />
<Materials c={c.materials} /> <Materials c={c.materials} />
<Typography c={c.typography} />
<Showcase c={c.showcase} />
<Metrics c={c.metrics} /> <Metrics c={c.metrics} />
<Preflight c={c.preflight} /> <Preflight c={c.preflight} />
<Typography c={c.typography} />
<Rules c={c.rules} /> <Rules c={c.rules} />
<Install c={c.install} ui={c.ui} /> <Install c={c.install} ui={c.ui} />
</Base> </Base>