스튜디오 저널
프론트엔드 엔지니어링, 미니멀 디자인, 그리고 프리미엄 디지털 경험을 만드는 기술적 장인 정신에 대한 심층 탐구.
I write my date pickers by hand — no react-day-picker, no date-fns, no dayjs. Radix supplies popover positioning and focus management; the 42-cell month, the week offset and the month arithmetic are about forty lines of pure functions. That held fine for one grid. The moment I rendered two months side by side, the arrow keys started teleporting. Each grid draws six full weeks, so January's trailing cells and February's leading cells are the same fourteen days, and data-date existed twice in the DOM. querySelector returns the first match, so focus kept landing in the wrong grid. The fix is to render the adjacent-month days as inert blanks — but only when a second grid is on screen.
Next renders <link rel="alternate" hrefLang> through React DOM, which writes the prop name verbatim, in camelCase — so grep hreflang on the served HTML returns zero and you conclude the tags were never emitted. They were. The two failures that actually cost indexation are quieter: canonicalizing every locale to /en, which discards the cluster, and a route-level alternates object, which replaces the layout's rather than merging into it. All three are catchable with one pass over the build output, which is the only artifact that tells the truth.
pnpm 11 finished moving its supply-chain settings into pnpm-workspace.yaml — that is now the only place they are read from — and three of them accept a shape that looks right and does nothing. allowBuilds is a map, not a list — write it as a YAML array and pnpm turns your entry into a package literally named '0'. trustPolicyIgnoreAfter counts minutes, so 365 means six hours, not a year, and quietly turns trustPolicy: no-downgrade into a no-op. And pnpm 11 no longer reads the pnpm field of package.json at all — it warns once and exits 0. None of the four fixes a CVE in a version you pinned. That is a different problem with a different fix.
A script rmSync'd a source file I had never committed — no git blob, no recycle bin, no editor backup. Most .js.map files Next.js writes into .next/ carry a sourcesContent array holding the original, pre-transform text of each module they compiled, so a project that has been built once since the file existed still has that file on disk, inside the build output. I got 660 lines back byte-for-byte. The lesson underneath is the one that comes first: back up every file a destructive script can touch, not just the ones git can restore.
shadcn 4.x generates components on top of Base UI instead of Radix. asChild is gone, and Accordion no longer accepts type or collapsible — both are TS2322, so tsc hands them to you in the first run. The one that survives a green build is Select: Base UI's <Select.Value> renders the value, not the selected item's children, unless you pass an items map to <Select.Root>. No error, no warning, and the open popup still looks correct — only the closed trigger reads buy where it should read Buy a template.
next/font/google는 woff2 파일을 fonts.gstatic.com에서 빌드 타임에 내려받는다. Google이 파일 해시를 교체하자 deploy가 404로 실패하기 시작했는데, 로컬에서는 재현할 수 없었다. 같은 fetch가 몇 초 전에 내 머신에서는 이미 성공했기 때문이다. 해결책: 폰트를 저장소에 직접 넣고(vendoring) next/font/local을 쓰는 것. 그러면 그 바이트는 내가 소유한 입력이 된다.
filter 와 backdrop-filter 의 경우, Lightning CSS 는 접두사가 붙은 선언과 붙지 않은 선언을 하나로 병합하면서 마지막에 쓴 쪽의 접두사 플래그만 남긴다. 그래서 backdrop-filter 다음에 -webkit-backdrop-filter 를 쓰면 WebKit 전용으로 컴파일된다. Chrome 은 -webkit-backdrop-filter 를 아예 지원하지 않기 때문에, 소스는 멀쩡해 보이는데 프로덕션에서 blur 만 조용히 사라진다. 접두사가 붙는 다른 속성 대부분은 똑같이 해도 멀쩡한데, 바로 그 점 때문에 이 문제는 놓치기 쉽다.
Tailwind v4에서는 @theme 안에 --z-overlay: 200을 써도 z-overlay 유틸리티가 만들어지지 않는다. 유틸리티는 네임스페이스에서 값을 읽고, z-가 보는 네임스페이스는 --z-index-다. 에러도 없고 경고도 없다. 클래스가 그냥 존재하지 않고, 요소는 z-index: auto로 떨어지고, 양수 z-index를 가진 건 뭐든 그 위에 그려진다 — 어떤 라우트에서는 그렇고, 어떤 라우트에서는 아니다. 확실하게 결론을 내주는 확인 방법은 소스가 아니라 빌드된 CSS에서 클래스 이름을 grep하는 것이다.
첫 렌더가 document, matchMedia, location.hash, localStorage, 쿠키에 의존한다면 값 하나만 어긋나도 곧바로 hydration mismatch다. 이 API들은 SSR 중에는 없고, 클라이언트의 첫 렌더에서는 이미 진짜 값이기 때문이다. 그런데도 tsc, ESLint, Node 환경 유닛 테스트는 전부 통과한다. 셋 중 어느 것도 hydration 렌더를 하지 않으니까. 정직한 해법은 둘이다. 마운트 후에 seed 하거나(간단하지만 한 프레임 깜빡인다), 값을 서버에서 읽거나(깜빡임은 없지만 쿠키 왕복이 필요하다).
params는 Promise가 됐고, metadata는 필드 단위로 병합될 뿐 깊게 병합되지 않으며, PageProps/LayoutProps는 무언가가 생성해 주기 전까지 존재하지 않고, 페인트 전에 도는 테마 스크립트는 서버에서 렌더링돼야 한다. 넷 중 셋은 조용히 깨진다. build 초록불, 타입 초록불, 그런데 HTML이 틀렸다. 아래 내용은 전부 내가 관리하는 Next.js 16.2.10 / React 19.2.7 코드베이스 5개에서 나온 것이다.
뉴스레터
새 템플릿을 출시하거나 가치 있는 글을 쓸 때 이메일을 보내드립니다. 잡음 없이.