Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
50190e3
refactor(blog): SsgoiTransition → div data-ssgoi-transition 전환
Han5991 Jun 28, 2026
00e74dd
refactor(blog): data-ssgoi-transition을 PageBoundary 컴포넌트로 추출
Han5991 Jun 28, 2026
19375cc
docs(blog): TS6 글에 참여 유도(CTA) 추가 + 파일명 정리
Han5991 Jun 28, 2026
b33052a
feat(blog): about 페이지 통계 실시간 연동
Han5991 Jun 28, 2026
f999ef1
refactor(blog): PageBoundary props를 div 속성으로 확장
Han5991 Jun 28, 2026
4f936b4
chore(blog): package.json engines 필드 제거
Han5991 Jun 28, 2026
eadc4c8
refactor(blog): useViewCount import 정렬 + 쿠키 문자열 인라인
Han5991 Jun 28, 2026
2a2d94f
feat: code 수정
Han5991 Jun 28, 2026
9ecd474
fix(next.js): vitest 4.1에서 jest-dom 매처 타입 보강 복구
Han5991 Jun 28, 2026
739a196
fix(blog): TS6 글 디렉터리 트리를 file-tree로 전환 — 깨진 인라인 박스 해결
Han5991 Jun 28, 2026
0bcea67
fix(blog): 언어 태그 없는 코드블록을 인라인이 아닌 블록으로 렌더
Han5991 Jun 28, 2026
3371433
docs(blog): Panda·bundler 글 트리를 file-tree로 전환 + TS6 맺음말 정리
Han5991 Jun 28, 2026
ed81199
fix(next.js): vitest 보강 타입에서 any 제거 — unknown/T로 정밀화
Han5991 Jun 28, 2026
373a15b
feat: code 수정
Han5991 Jun 28, 2026
122ab73
refactor(blog): PageBoundary id prop을 transitionId로 변경 + div props 포워딩
Han5991 Jun 28, 2026
394d24a
chore(next.js): vitest와 @vitest/expect 버전을 4.1.9로 정합
Han5991 Jun 28, 2026
7c8f4fe
refactor(next.js): 표준 jest-dom/vitest 설정으로 전환 — 커스텀 보강 제거
Han5991 Jun 28, 2026
b02646c
fix(blog): 한 줄짜리 언어 없는 fenced 코드블록도 블록으로 렌더
Han5991 Jun 28, 2026
a2f12c5
refactor(blog): gemini 리뷰 반영 — CRLF 안전 판별 + PageBoundary 폴더 구조
Han5991 Jun 28, 2026
2bf6728
fix(blog): CodeBlock 블록 판별 단순화 — rawContent가 \n으로 끝나면 fenced 블록
Han5991 Jun 28, 2026
003c4f0
fix(blog): CodeBlock 블록 판별을 endsWith('\n')으로 최소화
Han5991 Jun 28, 2026
2aa6d59
fix(blog): CodeBlock에서 children 비문자열 방어 — typeof 가드
Han5991 Jun 28, 2026
b8e410c
fix(blog): claude-code-review 반영 — 예약 배포 cron 시각 버그 외 4건
Han5991 Jun 28, 2026
d452f2e
fix(blog): claude 리뷰 🔴 2건 — code children 텍스트 추출 + 블록 판별 통일
Han5991 Jun 28, 2026
fadeccd
refactor(blog): CodeBlock 블록 판별을 isBlockCode 공유 헬퍼로 추출
Han5991 Jun 28, 2026
de987e3
fix(blog): claude 리뷰 잔여 2건 — 블록 판별 isBlockCode 통일 + JSON-LD <script> …
Han5991 Jun 28, 2026
2b7f27b
fix(blog): claude 재리뷰 2건 — PR수 검증 정규식 + codeText/FileTree 중복 제거
Han5991 Jun 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/deploy-blog.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,26 @@ jobs:
id: setup_pages
uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0

# about 페이지 'PR 승인' 통계 — Han5991/Han5991 프로필 레포가 매일 갱신하는
# config/summary.json(외부 OSS 머지 PR 총수)을 읽어 빌드 타임에 주입한다.
# 조회 실패 시 빈값 → 페이지 폴백(|| '58')이 처리.
- name: Fetch merged PR count
id: pr_count
run: |
json=$(curl -fsSL https://raw.githubusercontent.com/Han5991/Han5991/main/config/summary.json || true)
count=$(printf '%s' "$json" | jq -r '.mergedPRs // empty' 2>/dev/null || true)
[[ "$count" =~ ^[1-9][0-9]*$ ]] || count=''
echo "count=$count" >> "$GITHUB_OUTPUT"
echo "Merged PR count: ${count:-(fallback)}"

- name: Build blog
run: pnpm build --filter=@blog/web --no-cache
env:
PAGES_BASE_PATH: ${{ steps.setup_pages.outputs.base_path }}
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
NEXT_PUBLIC_ADMIN_EMAIL: ${{ secrets.NEXT_PUBLIC_ADMIN_EMAIL }}
NEXT_PUBLIC_PR_COUNT: ${{ steps.pr_count.outputs.count }}
NODE_ENV: production

- name: Upload to pages artifact
Expand Down
8 changes: 5 additions & 3 deletions apps/blog/posts/Panda CSS 1년 사용기.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,10 +330,12 @@ https://github.com/cschroeter/park-ui
- 공통 디자인 토큰 및 레시피 중앙 관리
Panda CSS의 panda.config.ts 파일에서 색상, 폰트, 간격 등의 디자인 토큰을 정의하고, 이를 레시피(예: 버튼, 텍스트 등)와 패턴으로 만들어 둡니다. 이 공통 디자인 시스템을 하나의 패키지로 구성하여, 모노레포 내 여러 프로젝트에서 재사용할 수 있습니다

<file-tree>
📦 packages
├── 📂 panda-preset
├── 📂 react
└── 📂 styled-system
📂 panda-preset
📂 react
📂 styled-system
</file-tree>

```typescript
// 예시: panda.config.ts (모노레포 공통 디자인 시스템)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,12 @@ private transformImportDeclaration(node) {

이 시리즈의 실습 코드는 다음 경로에서 확인할 수 있습니다.

```text
<file-tree>
packages/@package/bundler/
├── src/index.ts # 진입점: 설정 -> 빌드 -> 생성
├── src/Graph.ts # 의존성 그래프 구축 및 번들 생성
└── src/Module.ts # 개별 파일 AST 파싱 및 변환
```
src/index.ts # 진입점: 설정 -> 빌드 -> 생성
src/Graph.ts # 의존성 그래프 구축 및 번들 생성
src/Module.ts # 개별 파일 AST 파싱 및 변환
</file-tree>

### 단계별 참조 파일

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,13 @@ tsconfig.json:10:5 - error TS5011: The common source directory of 'tsconfig.json
그런데 방금 추가한 `rootDir: "./src"` 한 줄은 정확히 뭘 할까? `rootDir`은 **출력 폴더(`dist`)의 모양을 어디서부터 베낄지** 정하는 '깃발'이라
보면 된다. tsc는 깃발 **아래**의 폴더 구조를 그대로 `dist`에 복제한다.

```
<file-tree>
my-lib/
├── tsconfig.json
└── src/
├── index.ts
└── utils.ts
```
tsconfig.json
src/
index.ts
utils.ts
</file-tree>

깃발을 `src/`에 꽂으면 → `dist/index.js`, `dist/utils.js`. 한 칸 위(`my-lib/`)에 꽂으면 → `dist/src/index.js`로 `src/`가 딸려
들어온다. **위치 한 끗이 산출물 모양을 가른다.**
Expand Down Expand Up @@ -423,7 +423,7 @@ types</code> 기본값 변경을 되돌리는 옵션이 아니다. 옛 동작이

## 5. 보너스: 다시 튀어나온 baseUrl — 이번엔 내 코드가 아니었다

`pnpm build`가 디자인 시스템 패키지의 dts 빌드에 다다르자 **또 `TS5101`**이 떴다. 그런데 `@design-system/ui`의 tsconfig엔
`pnpm build`가 디자인 시스템 패키지의 dts 빌드에 다다르자 **또 `TS5101`** 이 떴다. 그런데 `@design-system/ui`의 tsconfig엔
`baseUrl`이 **없다.** #1에서 봤듯 TS5101은 범용 진단이니, 누군가 내 빌드에 baseUrl을 **주입**하고 있다는 뜻이다.

범인은 dts 번들러로 쓰던 **tsup**이었다. [소스](https://github.com/egoist/tsup/blob/main/src/rollup.ts)의 dts 빌드 옵션
Expand All @@ -444,17 +444,6 @@ baseUrl 주입은 내 코드가 아니라 도구의 문제이고, 그 도구가
`typescript: "^5.0.0 || ^6.0.0"`을 선언해 **TS6를 공식 지원**한다. 마이그레이션 도구(`npx tsdown-migrate`)로 config도 거의
그대로 옮겨졌고, 전환 직후 `TS5101`은 사라졌다.

다만 공짜는 아니었다. 전환하며 밟은 함정 셋:

- **출력 확장자.** tsdown은 `platform: 'node'`에서 `.mjs`/`.cjs`/`.d.mts`/`.d.cts`로 낸다(tsup은 `.js`/`.d.ts`).
`package.json`의 `exports`·`bin`·`main`·`types`를 산출물에 맞춰 전부 정정해야 했다.
- **`platform` ≠ `target`.** `platform: 'node'`는 의존성 외부화·출력 확장자 힌트일 뿐, ES 문법 타겟은 `target`이 따로 통제한다.
Node 24 문법까지 내리려면 `target: 'node24'`를 함께 명시.
- **빌드 도구는 `devDependencies`에.** `tsdown`·`typescript`는 런타임 의존성이 아니다. `@design-system/ui`가 `tsup`을
`dependencies`에 두고 있어 뒤늦게 옮겼다.

- `pnpm build` (전체) → **9/9 통과** (blog 정적 빌드 포함)

---

## 6. 정리: TypeScript 6 마이그레이션 체크리스트
Expand All @@ -481,11 +470,15 @@ baseUrl·rootDir·types 셋 다 5에서도 오늘 당장 적용할 수 있는
- **`rootDir` 고정** → 명시해 출력 레이아웃을 결정적으로 묶는다.
- **`types` 좁히기** → `types: ["node", ...]`로 명시해 자동 `@types` 열거 비용을 미리 던다.

세 줄 다 5.x tsconfig에서 오늘 커밋할 수 있다. 6은 이걸 '강제'했을 뿐 '발명'한 게 아니다.
세 줄 다 5.x tsconfig에서 오늘 커밋할 수 있다. 6은 이걸 **'강제'** 했을 뿐 **'개발'** 한 게 아니다.

---

## 그래서, 오늘 당신의 tsconfig는?

이 글을 닫기 전에 딱 하나만 하자 — `tsconfig.json`을 여는 것. `baseUrl`이 아직 남아 있는지, `rootDir`이 비어 있는지, `types`가 통째로 열려 있는지. 셋 다 6을 기다릴 것 없이 오늘 5.x에서 고치고, 그 커밋 하나로 빌드를 더 빠르게 만들 수 있다. 미뤄도 빌드는 돌지만, 미룬 만큼 7.0에서 한꺼번에 청구된다.

> 시작은 "이걸 왜 굳이 바꾸지?"라는 호기심 한 줄이었다. 세 옵션의 '왜'를 PR diff까지 따라가 보니, 답은 늘 같은 곳을 가리켰다 — Go로 다시 쓰인 7.0.
`baseUrl`·`types`·`rootDir`도, 마지막에 튀어나온 `tsup`도, 전부 그 길목을 미리 쓸어두는 일이었다. 그리고 가장 김빠지면서도 든든한 깨달음은 따로
> 있었다. 이 길, 6을 올려야만 걸을 수 있는 게 아니다. 5에서 그대로, 그것도 빌드가 빨라지는 채로 갈 수 있다. 버전 숫자를 올리는 일과 더 나은 설정으로 가는 일은, 생각보다 자주 별개다.
그리고 정말 궁금하다 — **여러분의 프로젝트는 이 셋 중 몇 개를 이미 지키고 있었나?** 셋 다 비어 있었다면 무엇부터 손볼지, 아니면 "우린 진작 package.json `imports`로 넘어갔다" 같은 이야기가 있다면 댓글로 남겨 달라. 남의 tsconfig가 어디서 새는지는, 의외로 서로의 댓글에서 가장 빨리 배운다.

---

Expand Down
7 changes: 2 additions & 5 deletions apps/blog/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@
"supabase-stop": "supabase stop --all --no-backup --debug",
"supabase-start": "pnpm supabase-stop && pnpm supabase start"
},
"engines": {
"node": ">=22"
},
"dependencies": {
"@design-system/ui": "workspace:^",
"@design-system/ui-lib": "workspace:^",
Expand Down Expand Up @@ -65,7 +62,7 @@
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^5.0.4",
"@vitejs/plugin-react": "^6.0.3",
"babel-plugin-react-compiler": "^1.0.0",
"c8": "^11.0.0",
"concurrently": "^9.2.0",
Expand All @@ -77,6 +74,6 @@
"supabase": "^2.70.5",
"tsx": "^4.22.0",
"typescript": "catalog:",
"vitest": "^4.0.0-beta.15"
"vitest": "^4.1.9"
}
}
19 changes: 13 additions & 6 deletions apps/blog/web/src/app/about/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import Link from 'next/link';
import { css } from '@design-system/ui-lib/css';
import { SsgoiTransition } from '@ssgoi/react';
import type { Metadata } from 'next';
import {
SITE_URL,
SITE_AUTHOR_GITHUB,
SITE_AUTHOR_LINKEDIN,
} from '@/lib/constants';
import { Label } from '@/src/components/blog';
import { PageBoundary } from '@/src/components/PageBoundary';
import { getAllPostSummaries } from '@/domain/post';

export const metadata: Metadata = {
title: '소개 | Frontend Lab',
Expand Down Expand Up @@ -59,13 +60,19 @@ const jsonLd = {
};

export default function AboutPage() {
// 빌드 타임 통계. 블로그 글 수는 실제 발행 수, PR 수는 CI가 주입(NEXT_PUBLIC_PR_COUNT),
// 로컬 빌드·CI 조회 실패 시 폴백 상수가 처리. 컨퍼런스는 수동 관리.
const blogPostCount = getAllPostSummaries().length;
const mergedPrCount = process.env.NEXT_PUBLIC_PR_COUNT || '58';
const conferenceCount = '2';

return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<SsgoiTransition id="/about">
<PageBoundary transitionId="/about">
<div className={css({ bg: 'paper.50' })}>
{/* Header */}
<header
Expand Down Expand Up @@ -213,9 +220,9 @@ export default function AboutPage() {
})}
>
{[
{ value: '33', label: '블로그 포스트' },
{ value: '38', label: 'PR 승인' },
{ value: '3', label: '컨퍼런스' },
{ value: String(blogPostCount), label: '블로그 포스트' },
{ value: mergedPrCount, label: 'PR 승인' },
{ value: conferenceCount, label: '컨퍼런스' },
].map(stat => (
<div key={stat.label}>
<div
Expand Down Expand Up @@ -620,7 +627,7 @@ export default function AboutPage() {
</div>
</div>
</div>
</SsgoiTransition>
</PageBoundary>
</>
);
}
6 changes: 3 additions & 3 deletions apps/blog/web/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import Link from 'next/link';
import { css } from '@design-system/ui-lib/css';
import { SsgoiTransition } from '@ssgoi/react';
import type { Metadata } from 'next';

import { getAllPostSummaries } from '@/domain/post';
Expand All @@ -22,6 +21,7 @@ import {
SearchBox,
Label,
} from '@/src/components/blog';
import { PageBoundary } from '@/src/components/PageBoundary';

export const metadata: Metadata = {
title: 'Frontend Lab | 프론트엔드 실험실',
Expand Down Expand Up @@ -141,7 +141,7 @@ export default function HomePage() {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<SsgoiTransition id="/">
<PageBoundary transitionId="/">
<div className={css({ bg: 'paper.50' })}>
<Hero />

Expand Down Expand Up @@ -351,7 +351,7 @@ export default function HomePage() {
</div>
</section>
</div>
</SsgoiTransition>
</PageBoundary>
</>
);
}
10 changes: 6 additions & 4 deletions apps/blog/web/src/app/posts/[...slug]/PostClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import rehypeSlug from 'rehype-slug';
import { css } from '@design-system/ui-lib/css';
import { SsgoiTransition } from '@ssgoi/react';

import type { PostData } from '@/domain/post';
import GiscusComments from '@/src/components/GiscusComments';
import { PageBoundary } from '@/src/components/PageBoundary';
import { useViewCount } from '@/src/hooks/useViewCount';
import { useRecordRecentView } from '@/src/hooks/useRecentViews';
import { BackToTop } from '@/src/components/mobile/BackToTop';
Expand Down Expand Up @@ -51,10 +51,12 @@ export default function PostClient({
<MobileTOC />
</div>

<SsgoiTransition
<PageBoundary
// 썸네일 있으면 /posts/*(hero 모핑 대상), 없으면 /posts-plain/*(fade 폴백)으로
// 분기해 전환 매칭을 라우팅한다. (URL은 그대로 /posts/{slug})
id={thumbnailUrl ? `/posts/${post.slug}` : `/posts-plain/${post.slug}`}
transitionId={
thumbnailUrl ? `/posts/${post.slug}` : `/posts-plain/${post.slug}`
}
className={css({
maxW: 'articleW',
mx: 'auto',
Expand Down Expand Up @@ -349,7 +351,7 @@ export default function PostClient({

<TOC />
</div>
</SsgoiTransition>
</PageBoundary>
</>
);
}
6 changes: 3 additions & 3 deletions apps/blog/web/src/app/posts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Suspense } from 'react';
import { css } from '@design-system/ui-lib/css';
import { SsgoiTransition } from '@ssgoi/react';
import type { Metadata } from 'next';

import { getAllPostSummaries } from '@/domain/post';
import { getAllSeries, getAllTags, getAllYears } from '@/domain/post/aggregate';
import { SITE_URL } from '@/lib/constants';
import { Label, PostsArchiveView } from '@/src/components/blog';
import { PageBoundary } from '@/src/components/PageBoundary';

export const metadata: Metadata = {
title: '모든 노트 | Frontend Lab',
Expand Down Expand Up @@ -88,7 +88,7 @@ export default function PostsPage() {
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(blogJsonLd) }}
/>
<SsgoiTransition id="/posts">
<PageBoundary transitionId="/posts">
<div
className={css({
maxW: 'containerW',
Expand Down Expand Up @@ -165,7 +165,7 @@ export default function PostsPage() {
/>
</Suspense>
</div>
</SsgoiTransition>
</PageBoundary>
</>
);
}
6 changes: 3 additions & 3 deletions apps/blog/web/src/app/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { css } from '@design-system/ui-lib/css';
import { SsgoiTransition } from '@ssgoi/react';
import type { Metadata } from 'next';
import { SITE_URL } from '@/lib/constants';
import { PageBoundary } from '@/src/components/PageBoundary';

export const metadata: Metadata = {
title: '개인정보처리방침 | Frontend Lab',
Expand All @@ -18,7 +18,7 @@ const LAST_UPDATED = '2026년 3월 15일';

export default function PrivacyPage() {
return (
<SsgoiTransition id="/privacy">
<PageBoundary transitionId="/privacy">
<div
className={css({ minHeight: '[calc(100lvh - 231px)]', bg: 'paper.50' })}
>
Expand Down Expand Up @@ -241,6 +241,6 @@ export default function PrivacyPage() {
</div>
</div>
</div>
</SsgoiTransition>
</PageBoundary>
);
}
14 changes: 14 additions & 0 deletions apps/blog/web/src/components/PageBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ComponentPropsWithRef } from 'react';

interface PageBoundaryProps extends ComponentPropsWithRef<'div'> {
/**
* `data-ssgoi-transition` 값 — 라우트 전환(hero/fade)을 매칭하는 키.
* 보통 해당 페이지 경로("/posts" 등)를 쓴다.
* (HTML 표준 `id` 속성과 충돌하지 않도록 별도 이름을 쓴다.)
*/
transitionId: string;
}

export const PageBoundary = ({ transitionId, ...rest }: PageBoundaryProps) => (
<div data-ssgoi-transition={transitionId} {...rest} />
);
Loading