Skip to content

docs(blog): TypeScript 6 삽질기 글 정비 + 인라인 code 렌더링 수정#129

Merged
Han5991 merged 28 commits into
mainfrom
feat/typescript-update
Jun 27, 2026
Merged

docs(blog): TypeScript 6 삽질기 글 정비 + 인라인 code 렌더링 수정#129
Han5991 merged 28 commits into
mainfrom
feat/typescript-update

Conversation

@Han5991

@Han5991 Han5991 commented Jun 27, 2026

Copy link
Copy Markdown
Owner

개요

PR #128(TypeScript 6 + 빌드 툴체인 업데이트) 이후 작업으로, TypeScript 6 업그레이드 삽질기 블로그 글을 정비하고, 그 글을 쓰면서 발견한 마크다운 렌더링 버그를 함께 수정합니다.

변경 사항

블로그

  • apps/blog/posts/typescript/typescript-6-업그레이드-삽질기.md — 글 전반 정비 (489 라인 변경)
    • baseUrl/rootDir/types 기본값 변경의 "왜"를 microsoft/TypeScript PR diff까지 추적한 내용으로 재구성
    • TS5에서도 오늘 당장 적용 가능하다는 관점으로 결론 정리
    • 예약 발행: status: scheduled, scheduledDate: 2026-07-02T09:00:00+09:00 (KST 09:00 cron 빌드에서 자동 공개)
  • 썸네일 이미지(typescript-6-migration-thumb.png) 추가 및 THUMBNAIL_LOG.md 기록
  • admin-posts-index.json 갱신

버그 수정

  • PostClient.tsx — 마크다운 단락(<p>) 안의 인라인 code가 블록 요소로 오판되던 문제 수정. isBlockElementnode.tagName === 'code'를 명시적으로 제외하도록 변경해, 인라인 코드가 포함된 단락이 잘못 분해되지 않도록 함.

기타

  • .idea/ 디렉터리 git 추적 제외 (12개 파일 삭제)
  • .tool-versions 갱신

발행 동작

  • 글이 status: scheduled이므로, 이 PR을 머지해도 즉시 공개되지 않습니다.
  • 예약 시각(2026-07-02 09:00 KST)이 지난 뒤 매일 KST 09:00 cron 빌드가 돌면 자동으로 공개됩니다. (isPostVisible: status가 있으면 published 필드는 무시)
  • 더 빨리/늦게 공개하려면 scheduledDate만 조정하면 됩니다.

참고

  • 이 브랜치 diff에 대한 코드 리뷰가 백그라운드에서 진행 중입니다 (결과는 별도 보고 예정).

🤖 Generated with Claude Code

Han5991 and others added 20 commits June 16, 2026 21:42
…Node 버전 동기화

- @design-system/ui: 최상위 main/types가 tsup 시절 .js/.d.ts를 가리켜 dangling 참조였음. tsdown은 .mjs/.d.mts만 산출하므로 publishConfig.exports와 일치시킴
- CLAUDE.md: engines.node >=24 반영 (Node >=22 → >=24), pnpm 10.10.0 → 11.6.0 동기화

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
baseUrl deprecation(TS5101)·types 기본값 []·rootDir 요구(TS5011)를 릴리스 노트·이슈를 넘어 실제 microsoft/TypeScript PR diff(#62509·#63054·#62418)까지 추적하는 탐구 서사로 전면 재작성. 보너스로 유지보수가 멈춘 tsup을 tsdown으로 전환한 과정까지.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
scripts/build-content.ts가 prebuild/predev에서 재생성하는 public/ 산출물을 .gitignore에 추가하고 추적에서 제거. 빌드 시점에 항상 새로 생성되므로 버전 관리 불필요.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- named catalog(react19) → 기본 catalog로 react·react-dom·@types 통합 (stable 19.2.x)
- apps/{react,blog-web,next.js}·@design-system/ui의 catalog:react19 → catalog: 전환
- @design-system/ui: 빌드 도구(tsdown·typescript)를 dependencies → devDependencies (동일 파일이라 함께 반영)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @package/bundler: tsdown.config에 target: 'node24' 명시 (platform과 별개 축)
- @package/core: 루트(pnpm@11.6.0)와 어긋난 packageManager(pnpm@10.4.1) 필드 제거
- blog: tsup→tsdown 보너스에 함정(platform≠target, 빌드도구 devDeps) + 체크리스트 보강

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- 도입부(회사 리허설→부채→TS7 Go 청소)와 결말 수미상관 구성
- baseUrl→package.json imports 곁가지(details), tsup 5장 압축
- '정작 가장 큰 수확'을 7장으로 승격 + 정리/액션 재구성
- excerpt 압축·tags 보강 등 SEO, 6렌즈 평가 기반 구조 개선

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
# Conflicts:
#	apps/blog/posts/typescript/typescript-6-업그레이드-삽질기.md
@vercel

vercel Bot commented Jun 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ant-blog Ready Ready Preview, Comment Jun 27, 2026 5:31pm
fe-lab-next-js Ready Ready Preview, Comment Jun 27, 2026 5:31pm

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 PR은 TypeScript 6 마이그레이션 관련 블로그 포스트의 내용을 최신화하고, 프로젝트 내 불필요한 IDE 설정 파일을 정리하며 전반적인 문서 및 환경 설정을 정비하는 것을 목적으로 합니다.

Highlights

  • 블로그 포스트 최신화: TypeScript 6 마이그레이션 경험을 다룬 블로그 포스트의 제목, 내용, 태그 및 메타데이터를 대폭 수정하고 정비했습니다.
  • 프로젝트 환경 정리: 불필요한 .idea 설정 파일들을 제거하고, .tool-versions의 Node.js 버전을 24.17.0으로 업데이트했습니다.
  • 인덱스 및 로그 업데이트: 블로그 포스트 인덱스(admin-posts-index.json)에 새 글을 추가하고, 썸네일 로그(THUMBNAIL_LOG.md)를 갱신했습니다.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

이번 풀 리퀘스트는 불필요한 IDE 설정 파일들을 정리하고 Node.js 버전을 24.17.0으로 업데이트하며, TypeScript 6 마이그레이션 삽질기를 다룬 기술 블로그 포스트를 추가 및 보완합니다. 리뷰어 피드백에서는 마크다운 문서 내 코드 블록 지시어의 적절한 사용(json 대신 jsonc), 인라인 코드 블록 주변의 공백 추가, 줄바꿈으로 인한 볼드체 강조 기호 분리 수정, 그리고 JSON 스니펫 내 불필요한 trailing comma 제거 등 문서의 가독성과 구문 정확성을 높이기 위한 유용한 개선 사항들이 제시되었습니다.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

`baseUrl`이 deprecated된다는 건 이미 알고 있었다. 내 코드에서 `baseUrl`을 실제로 쓰는 곳은 React 앱 하나뿐이었고, `paths`의 접두사 용도였다.

```jsonc
```json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

코드 블록 내부에 주석(// ...)이 포함되어 있습니다. 표준 JSON은 주석을 지원하지 않으므로, 마크다운 렌더러나 린터가 구문 오류를 발생시키지 않도록 코드 블록 지시어를 json 대신 jsonc(JSON with Comments)로 변경하는 것이 좋습니다.

```

그냥 지웠다. `paths`는 [TypeScript 4.1부터 `baseUrl` 없이 동작](https://www.typescriptlang.org/tsconfig/baseUrl.html)한다.
그냥 지웠다. `paths`는 [TypeScript 4.1부터`baseUrl` 없이 동작](https://www.typescriptlang.org/tsconfig/baseUrl.html)한다.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

조사('부터')와 인라인 코드 블록('baseUrl') 사이에 공백이 누락되어 '4.1부터baseUrl'로 붙어 있습니다. 가독성을 위해 '4.1부터 baseUrl'과 같이 공백을 추가하는 것이 좋습니다.

Comment on lines +148 to +149
`baseUrl`이 6.0에서 막힌 그 이유(런타임에 안 맞는 import를 타입체커만 통과)와 정도만 다를 뿐 같은 괴리를 안는다. 그 괴리가 구조적으로 없는 대안이 *
*Node가 런타임에 직접 읽는 package.json `imports`** (`#` subpath)다 — 타입과 런타임이 같은 매핑을 보니 어긋날 수 없고, TS 핸드북도

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

볼드체 강조 표시(**...**)가 줄바꿈으로 인해 **Node가로 분리되어 있습니다. 대부분의 마크다운 파서(GitHub Flavored Markdown 포함)에서는 줄바꿈으로 분리된 아스테리스크를 강조 기호로 인식하지 못하고 일반 텍스트로 렌더링하게 됩니다. 강조 기호가 분리되지 않도록 한 줄로 합치거나 줄바꿈 위치를 조정해 주세요.

+export function usesWildcardTypes(options: CompilerOptions): options is CompilerOptions & { types: string[] } {
+ return some(options.types, t => t === "*");
+}
```json

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

코드 블록 내부에 주석(// ...)이 포함되어 있으므로, 구문 강조와 린터 경고 방지를 위해 json 대신 jsonc 지시어를 사용하는 것을 권장합니다.

Comment on lines +205 to 206
"rootDir": "./src", // ← 추가
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

이전 코드에서 "allowJs": true 라인이 삭제되면서 "rootDir": "./src"compilerOptions 객체의 마지막 항목이 되었습니다. 표준 JSON 스펙에서는 마지막 항목 뒤에 쉼표(trailing comma)를 허용하지 않으므로, 이를 제거하는 것이 좋습니다.

Suggested change
"rootDir": "./src", // ← 추가
}
"rootDir": "./src" // ← 추가
}

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

PR Review: TypeScript 6 삽질기 글 정비 + 인라인 code 렌더링 수정

이 PR은 TypeScript 6 마이그레이션 블로그 글을 draft에서 published 상태로 전환하고, 섹션 순서 재배치 및 내용 보강을 포함합니다. 동시에 MDX 렌더러에서 인라인 code를 블록 요소로 잘못 분류하던 문제를 수정하려 했습니다. IntelliJ .idea/ IDE 파일도 정리됩니다.


🔴 중요: 예약 발행 메커니즘 오류

apps/blog/posts/typescript/typescript-6-업그레이드-삽질기.md — frontmatter (line 3-4)

date: '2026-07-02'
published: true

오늘(2026-06-27)로부터 5일 뒤 날짜를 date에 적었지만, isPostVisible()date 필드를 전혀 확인하지 않습니다. status가 없으면 published === true만 보고 즉시 공개 처리합니다 (domain/post/visibility.ts 참조). 이 PR이 머지되면 글은 sitemap, RSS, 포스트 목록에 지금 즉시 노출됩니다.

예약 발행이 의도라면 CLAUDE.md 정책대로 아래 형식을 사용해야 합니다:

status: 'scheduled'
scheduledDate: '2026-07-02T09:00:00+09:00'

🟡 isBlockElement 수정이 효과 없음

apps/blog/web/src/app/posts/[...slug]/PostClient.tsx — lines 272-275

const tagName = (
  node.props as { node?: { tagName?: string } }
)?.node?.tagName;
return tagName !== 'code';

react-markdown v10은 커스텀 컴포넌트에 node prop 주입을 제거했습니다 (react-markdown v10 changelog). 이 프로젝트는 "react-markdown": "^10.1.0"을 사용하므로 node.props.node는 항상 undefined이고, tagName도 항상 undefined입니다.

결과적으로 undefined !== 'code'는 항상 true이므로, 인라인 <code>는 수정 전과 동일하게 여전히 블록 요소로 분류됩니다. <p><div> 강제 변환 버그는 수정되지 않습니다.

올바른 접근은 v10 API에서 code 컴포넌트가 인라인인지 블록인지 구분하는 방식으로 — className prop 확인이나 커스텀 컴포넌트 타입 직접 비교(예: node.type === CodeBlock && !node.props.className)를 사용하는 것입니다.


🟡 admin-posts-index.json 수동 커밋

apps/blog/web/public/admin-posts-index.json — line 1

이 파일은 generate-search-index.tsprebuild 스크립트가 생성하는 빌드 산출물입니다. 수동으로 커밋하면:

  • 다음 CI/CD 빌드 시 스크립트가 파일을 덮어써, 수동 편집 내용이 사라집니다
  • 빌드 전까지의 배포 기간 동안 admin 대시보드가 스크립트 계산값과 다른 데이터를 표시할 수 있습니다

이 파일은 .gitignore에 추가하거나, 그냥 커밋하지 않는 것이 좋습니다.


🔵 .idea/ 정리 (참고)

삭제는 올바른 방향입니다. .gitignore에는 이미 /.idea/가 있으므로 (line 53) 추가 조치는 필요 없습니다. 다만 dataSources.xml에 Supabase PostgreSQL pooler 주소(aws-0-ap-northeast-2.pooler.supabase.com)가 포함되어 있었는데, git 히스토리에는 여전히 남아 있습니다. 파일에 자격증명(비밀번호/서비스롤 키)이 없었다면 무해하지만, 확인해 두시는 것을 권장합니다.


요약

# 파일 심각도 문제
1 typescript-6-...삽질기.md 🔴 published: true + 미래 날짜 → 즉시 공개됨. status: 'scheduled' + scheduledDate 필요
2 PostClient.tsx 🟡 react-markdown v10에서 node prop 제거로 tagName 체크 항상 undefined → 수정 무효
3 admin-posts-index.json 🟡 빌드 산출물 수동 커밋 → 다음 빌드 시 덮어써짐
4 .idea/dataSources.xml 🔵 DB URL이 git 히스토리에 잔존 (자격증명 여부 확인 권장)

published: true → status: scheduled + scheduledDate(2026-07-02T09:00+09:00).
status가 있으면 isPostVisible은 published를 무시하므로 published 필드 제거.
매일 KST 09:00 cron 빌드에서 예약 시각 도달 시 자동 공개됩니다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review

이 PR은 세 가지를 포함합니다: TypeScript 6 삽질기 블로그 포스트 개편 (draft → scheduled), 인라인 코드 렌더링 버그 수정 (PostClient.tsx), 그리고 .idea/ IDE 파일 정리. 코드 변경은 PostClient.tsx 하나뿐이며 나머지는 콘텐츠/설정 변경입니다.


🟡 apps/blog/web/public/admin-posts-index.json — 스테일 스냅샷 (stale generated file)

새로 추가된 포스트 엔트리의 값이 실제 frontmatter와 불일치합니다:

필드 admin-posts-index.json (커밋된 값) 실제 frontmatter
status "published" "scheduled"
scheduledDate null "2026-07-02T09:00:00+09:00"

이 파일은 scripts/generate-search-index.tsbuildAdminPostsIndex()가 자동 생성하는 파일입니다. frontmatter를 status: 'scheduled'으로 바꾼 후 스크립트를 재실행하지 않아 커밋 시점에 이미 stale한 상태가 됐습니다. 다음 빌드에서 자동 정정되지만, PR 상태에서 admin 대시보드를 확인하면 잘못된 published 상태로 표시됩니다.

권고: PR 머지 전에 pnpm blog-web(prebuild) 혹은 빌드 스크립트를 한 번 실행해 admin-posts-index.json을 재생성하고 커밋에 포함하세요.


🟢 apps/blog/web/src/app/posts/[...slug]/PostClient.tsx:265isBlockElement 수정 검증 완료

// Before
const isBlockElement = (node: unknown) =>
  isValidElement(node) && typeof node.type !== 'string';

// After
const isBlockElement = (node: unknown) => {
  if (!isValidElement(node) || typeof node.type === 'string') {
    return false;
  }
  const tagName = (node.props as { node?: { tagName?: string } })?.node?.tagName;
  return tagName !== 'code';
};

로직 검증: node.props.node?.tagName 경로가 실제로 hast 노드의 tagName에 도달하는지 확인했습니다. react-markdown은 custom 컴포넌트에 node prop으로 hast Element를 전달하므로, 이 경로는 올바릅니다. 인라인 코드(`foo`)의 경우 tagName === 'code'가 맞고, 고쳐진 버그(인라인 code가 block 취급되어 <p><div> 변환되던 문제)가 의도대로 해결됩니다.

한 가지 개선 가능 포인트: node.props as { node?: { tagName?: string } }는 매우 느슨한 타입 캐스트입니다. react-markdown의 hast Element 타입을 직접 import해서 사용하면 향후 API 변경을 TypeScript가 잡아줄 수 있습니다:

import type { Element } from 'hast';
// ...
const tagName = (node.props as { node?: Element })?.node?.tagName;

현재는 기능상 완전히 올바르므로 blocking 이슈는 아닙니다.


🟡 .idea/dataSources.xml — 프로덕션 DB URL이 git 이력에 기록됨

이번 PR에서 .idea/ 파일들을 정리하는 것 자체는 바람직합니다. 다만 dataSources.xml에는 프로덕션 Supabase DB 호스트(aws-0-ap-northeast-2.pooler.supabase.com)가 포함되어 있었고, 이 파일은 이전에 이미 git 이력에 커밋된 상태입니다. 자격증명(credentials)은 없으므로 즉각적인 보안 위험은 아니지만, .idea/.gitignore에 포함되어 있지 않아 발생한 일입니다.

권고: 루트 .gitignore.idea/를 추가하여 재발 방지 (이번 PR 범위가 아니라면 별도 PR로).


✅ 기타

  • .tool-versions Node.js 24.6.0 → 24.17.0 업데이트: 정상
  • thumbnail: 'typescript-6-migration-thumb.png' 경로 형식: resolveThumbnailUrl()/posts/{relativeDir}/를 자동으로 prefix하므로 시리즈 폴더 없이 파일명만 쓰는 것이 올바른 형식입니다.
  • 포스트 frontmatter의 status: 'scheduled' + scheduledDate: '2026-07-02T09:00:00+09:00' 조합: CLAUDE.md 규칙에 맞게 작성됨.

요약: admin-posts-index.json 재생성 후 재커밋을 권고합니다. PostClient.tsx 코드 수정은 검증 완료되었으며 올바릅니다.

generate-search-index.ts 생성기로 재생성하여 .md frontmatter(status:
scheduled, scheduledDate)와 동기화. 손으로 두었던 status:'published',
scheduledDate:null 항목을 정정.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isBlockElement(인라인 클로저, code 태그 denylist)를 모듈 레벨 순수 함수
isBlockMarkdownChild로 교체.

- fenced code(language-*)가 <p> 안에 들어올 때도 블록으로 인식해
  무효 중첩(<p><div>…</div></p>) → hydration mismatch 방지
- react-markdown 내부 node prop(node.props.node.tagName) 의존 제거.
  커스텀 블록 컴포넌트는 참조(identity), fenced code는 공개 prop className
  으로 식별 → 라이브러리 업그레이드 시 회귀 방지
- 인라인 이미지/코드 등 phrasing 콘텐츠를 더는 <div>로 오포장하지 않음
  (`& p` 마진/타이포 보존). denylist 1태그 설계 → allowlist로 전환

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review

This PR has two distinct parts: a bug fix in PostClient.tsx that corrects inline code rendering inside paragraphs, and a blog post rewrite of the TypeScript 6 migration article (content-only). The .idea/ removal and Node.js version bump are straightforward housekeeping.

The bug fix is the only code logic change, so that's where the review is focused.


✅ Fix is correct for its stated purpose

apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, line 265–276

The root cause was right: CodeBlock is a React function component, so the original typeof node.type !== 'string' check classified all inline backtick code as a block element, demoting <p> to <div>. The new check reads the hast AST tagName from node.props.node.tagName — which react-markdown passes as the node prop — and correctly excludes 'code' from the block-element list.

This works because fenced code blocks are block-level in standard Markdown and are never parsed as children of <p>, so the code component in a paragraph context is exclusively inline backtick code.


⚠️ Findings

1. Silent regression risk if node prop API changes
apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, line 272–275

const tagName = (node.props as { node?: { tagName?: string } })?.node?.tagName;
return tagName !== 'code';

If node.props.node is absent — which would happen if react-markdown/rehype changes how it passes the hast node (e.g., after a version bump) — then tagName is undefined. Since undefined !== 'code' is true, the function returns true and inline code is treated as a block element again, silently reverting the bug. The node prop is already destructured-but-ignored in CodeBlock.tsx (node: _node), so there's no compile-time protection either.

Suggestion: add a narrow guard or a comment explaining the dependency on the react-markdown node prop shape, so a future upgrade doesn't silently break this.

2. Implicit "unknown = block" default for future component overrides
apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, line 265–276

When tagName is undefined (any custom component without a hast tagName), the function returns true (block element). Currently safe because all custom components in the components map are genuinely block-level except code. But the logic is exclusion-based (tagName !== 'code'), so if a future contributor adds an override for an inline element — e.g., a: CustomLink — every paragraph containing a link would silently become a <div>. No test covers the p override today.

Suggestion: worth a brief comment alongside the tagName !== 'code' line explaining why code is the only excluded case (the only inline custom component registered), so the next contributor knows to update this when adding more overrides.

3. admin-posts-index.json is auto-generated
apps/blog/web/public/admin-posts-index.json, line 1–25

This file is regenerated on every pnpm build / pnpm dev by generate-search-index.ts. The manually added entry in this PR is content-correct and will match what the generator produces, so this specific PR is fine. The broader risk: there's no CI diff check to catch future manual edits that would be silently overwritten at deploy time. The file being both committed and auto-generated is the latent issue — a note or # DO NOT EDIT - generated by generate-search-index.ts header would prevent confusion.


No blocking issues

All three findings are either low-probability silent regressions or process notes. The fix itself is correct and targeted. The blog content rewrite is well-structured. Ready to merge with the above in mind.

Han5991 and others added 2 commits June 28, 2026 02:00
hast-util-to-jsx-runtime는 컴포넌트 type이 함수일 때(passNode:true)
hast node를 props.node로 주입한다. p/table/li 오버라이드가 이 props를
{...props}로 host 요소에 spread하면서 node="[object Object]" 속성이
렌더링됐다. 세 곳에서 node를 destructure로 걸러내 누출 제거.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
generate-search-index.ts가 prebuild/predev에서 매번 재생성하는 산출물.
형제 파일(search-index.json·sitemap.xml·rss.xml·llms-full.txt)은 이미
gitignore인데 이 파일만 추적돼 "커밋+자동생성" drift 위험이 있었다
(코드 리뷰 봇·워크플로 공통 지적). gitignore에 추가하고 추적 해제해
public/ 생성물 처리를 일관화한다. 배포 빌드가 재생성하므로 안전.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review

이 PR은 TypeScript 6 업그레이드 블로그 글을 draft → scheduled로 정비하고, PostClient.tsx의 마크다운 단락 렌더링 버그를 수정합니다. typeof node.type !== 'string'으로 모든 커스텀 컴포넌트를 블록으로 판단하던 방식을 명시적 allowlist(BLOCK_MARKDOWN_COMPONENTS)로 교체한 방향은 맞습니다. 다만 allowlist에 빠진 컴포넌트가 있어 원래 고치려던 버그가 이미지에서는 재현됩니다.


🔴 버그 (CONFIRMED): MarkdownImageBLOCK_MARKDOWN_COMPONENTS에서 누락

apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, line 33

// 현재
const BLOCK_MARKDOWN_COMPONENTS = new Set<unknown>([Callout, Figure, FileTree]);

img 핸들러는 MarkdownImage를 반환하고, MarkdownImagereact-medium-image-zoom<Zoom>으로 감싸 블록 레벨 <div> 를 렌더합니다. 그런데 MarkdownImage가 Set에 없고 className prop도 없어서 isBlockMarkdownChildfalse를 반환합니다.

결과: 포스트에 단독 이미지(![alt](img.png))가 있으면 SSR이 <p><div>…</div></p>를 내보내고, 브라우저가 <p>를 조기 종료해 SSR/CSR 트리가 어긋납니다 — 이 PR이 고치려는 hydration mismatch가 이미지에서는 그대로 남아 있습니다.

수정:

const BLOCK_MARKDOWN_COMPONENTS = new Set<unknown>([Callout, Figure, FileTree, MarkdownImage]);

🟡 사전 존재하는 문제 (pre-existing, 기회 수정 권장): node prop이 DOM으로 누출

apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, line ~289

p({ children, ...props }) {
  // ...
  return <div {...props}>{children}</div>;  // node: Element가 그대로 전달됨
  // 또는
  return <p {...props}>{children}</p>;
}

react-markdown v10은 모든 컴포넌트 핸들러에 node: Element(hast 객체)를 prop으로 넘깁니다. 이게 ...props에 포함되어 DOM 요소에 전달되면 React가 단락마다 Warning: React does not recognize the \node` prop on a DOM element를 찍습니다. 이 PR이 만든 문제는 아니지만, p()` 핸들러를 건드린 김에 같이 정리하면 좋습니다.

수정:

p({ children, node: _node, ...props }) { ... }

table, li 등 다른 핸들러도 같은 패턴이라 일괄 적용이 필요합니다.


🟡 타입 정밀도: Set<unknown>Set<React.ElementType>

apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, line 33

const BLOCK_MARKDOWN_COMPONENTS = new Set<unknown>([Callout, Figure, FileTree]);

unknown으로 타입이 지정돼 있어 TypeScript가 Set에 컴포넌트가 아닌 값이 들어가는 경우를 잡아주지 못합니다. 또한 line 49의 (child.props as { className?: string }) 캐스팅도 타입이 구체화되면 isValidElement<{ className?: string }>(child)로 대체할 수 있습니다.

const BLOCK_MARKDOWN_COMPONENTS = new Set<React.ElementType>([Callout, Figure, FileTree, MarkdownImage]);

✅ 잘 된 점

  • code 인라인 요소를 블록으로 오판하던 원인(allowlist 바깥의 모든 커스텀 컴포넌트를 블록 취급)을 정확히 짚었습니다.
  • react-markdown 내부 node prop에 의존하지 않고 공개 prop(className)과 컴포넌트 참조(identity)로만 판별하는 설계 — 라이브러리 업그레이드에 견고합니다.
  • fenced code(language-* className)를 별도로 처리한 것도 올바릅니다(<pre><code> 구조에서 p() 핸들러가 code 요소를 직접 자식으로 받는 경우를 커버합니다).
  • .idea/ 삭제 + 루트 .gitignore에 이미 /.idea/가 있어 재추적 우려 없음.
  • thumbnail 경로(typescript-6-migration-thumb.png bare filename)가 resolveThumbnailUrlrelativeDir 결합 방식과 일치합니다.

필수 수정: MarkdownImageBLOCK_MARKDOWN_COMPONENTS에 추가해야 이미지가 들어간 단락의 hydration mismatch가 해결됩니다.

🤖 Generated with Claude Code

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review

이 PR은 .idea/ 파일 정리, TypeScript 6 블로그 글 정비(예약 발행), 그리고 마크다운 인라인 code<p> 안에서 블록 요소로 오판되던 hydration mismatch 버그 수정이 핵심입니다. 블로그 글 내용과 인프라 변경(gitignore, admin-posts-index 자동화)은 깔끔합니다. 코드 수정 방향은 올바르나, 수정 과정에서 이미지 렌더링에 회귀(regression)가 발생했습니다.

[HIGH] MarkdownImage 회귀: <p><figure> 무효 중첩

파일: apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, 33번 줄

BLOCK_MARKDOWN_COMPONENTS 셋에 Callout, Figure, FileTree만 추가하고 img 핸들러는 제외했습니다. PR 코멘트에 "이미지 등 phrasing 콘텐츠는 <p> 안에 유효" 라고 적혀 있지만, MarkdownImage<Zoom>으로 감싸고 Zoom은 DOM에 <figure>를 렌더합니다. <figure>는 블록 요소라 <p> 안에 들어갈 수 없습니다.

  • 구버전 동작: img 핸들러 익명 함수는 typeof node.type !== 'string' = true → <div>로 감쌈 (올바름)
  • 신버전 동작: 익명 img 함수는 BLOCK_MARKDOWN_COMPONENTS에 없고, classNamelanguage-* 아님 → false → <p>로 감쌈 → <p><figure>...</figure></p> 무효 HTML → SSR/CSR 트리 불일치 → React hydration 에러

재현: 단락에 이미지 하나만 있는 markdown (예: ![alt](url) 단독 줄). 현재 블로그 포스트에 이 패턴이 많을 가능성이 높습니다.

수정 방법: img 핸들러를 익명 함수 대신 named 함수로 추출하고 Set에 추가합니다.

function ImgRenderer({ src, alt }: { src?: string; alt?: string }) {
  return <MarkdownImage src={src} alt={alt} relativeDir={relativeDir} />;
}

const BLOCK_MARKDOWN_COMPONENTS = new Set<React.ComponentType>([
  Callout, Figure, FileTree, ImgRenderer,
]);

// components 맵에서:
img: ImgRenderer,

[MEDIUM] className 기반 코드블록 감지가 라이브러리 변경에 취약

파일: apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, 50번 줄

현재 fenced code block 감지는 /\blanguage-/ regex로 className prop을 검사합니다. react-markdown이 code 컴포넌트에 className을 전달하는 방식은 라이브러리 구현 세부사항이라, 버전 업그레이드로 prop 이름이 바뀌면 CodeBlock<p> 안에 들어가 hydration mismatch가 납니다. code 핸들러도 named 함수로 추출해 Set에 추가하면 identity 기반으로 바뀌어 regex가 불필요합니다.

[LOW] Set<unknown> 타입을 Set<React.ComponentType>으로

파일: apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, 33번 줄

Set<React.ComponentType>으로 바꾸면 비컴포넌트 값이 실수로 들어갈 때 컴파일 타임에 잡힙니다.

[MINOR] JSDoc이 WHAT을 설명 (CLAUDE.md 위반)

파일: apps/blog/web/src/app/posts/[...slug]/PostClient.tsx, 42-53번 줄

CLAUDE.md: "Default to writing no comments. Only add one when the WHY is non-obvious." isBlockMarkdownChild JSDoc 두 번째 블록은 함수 이름이 이미 전달하는 내용을 반복합니다. WHY(hydration mismatch 방지)는 첫 번째 Set 주석에 이미 있으므로 두 번째 JSDoc은 제거해도 됩니다.


요약: HIGH 이슈(MarkdownImage 회귀)를 수정하지 않으면 이미지가 있는 포스트에서 hydration 에러가 발생합니다. img 핸들러를 named 함수로 추출해 BLOCK_MARKDOWN_COMPONENTS에 추가하는 것이 최소 수정입니다.

Generated with Claude Code

MarkdownImage는 react-medium-image-zoom <Zoom>의 블록 <div data-rmiz>를
렌더하므로 단독 이미지(![alt](url))가 <p><div></div></p> 무효 중첩을
만들었다. img는 closure(relativeDir)가 필요한 인라인 래퍼로 매핑돼
child.type이 래퍼 함수라 identity로 못 잡으므로(코드 리뷰 봇 제안의
Set 추가가 실제로는 매치 안 됨), code의 className 판별과 동일하게 공개
prop src로 식별하도록 보강.

- BLOCK_MARKDOWN_COMPONENTS: Set<unknown> → Set<ElementType> (컴포넌트가
  아닌 값이 들어가면 컴파일 단계에서 차단)
- isValidElement<{ className?; src? }>로 props 캐스팅 제거

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

PR Review

이 PR은 두 가지 작업을 합칩니다: (1) TypeScript 6 업그레이드 경험을 담은 블로그 글 정비 및 예약 발행 설정, (2) 마크다운 단락(<p>) 안의 인라인 code가 블록 요소로 오판되던 하이드레이션 버그 수정. .idea/ 디렉터리 제거와 admin-posts-index.json gitignore 추가도 포함합니다.


Findings

1. MarkdownImageBLOCK_MARKDOWN_COMPONENTS에 직접 추가할 수 있다 (Simplification)

파일: apps/blog/web/src/app/posts/[...slug]/PostClient.tsx:37–62

현재 이미지 식별은 src: string prop 휴리스틱으로 처리됩니다:

const BLOCK_MARKDOWN_COMPONENTS = new Set<ElementType>([Callout, Figure, FileTree]);
// ...
if (typeof src === 'string') return true;

주석에서는 "img는 인라인 래퍼(img({src}) => <MarkdownImage/>)라 child.type이 래퍼 함수라 identity로 못 잡는다"고 설명하지만, 이는 부정확합니다. img 렌더러 클로저가 반환하는 JSX <MarkdownImage src="..." />type은 클로저 함수 자체가 아니라 임포트된 MarkdownImage 컴포넌트입니다. 즉 child.type === MarkdownImage로 identity 비교가 가능합니다.

// 개선안
const BLOCK_MARKDOWN_COMPONENTS = new Set<ElementType>([
  Callout,
  Figure,
  FileTree,
  MarkdownImage, // img 렌더러가 반환하는 JSX의 type이 MarkdownImage
]);

// isBlockMarkdownChild에서 src 휴리스틱 제거 가능

src: string 휴리스틱은 현재 코드에서는 동작하지만, 미래에 src prop을 가진 다른 인라인 컴포넌트가 추가되면 잘못 블록 처리될 수 있습니다.


2. CLAUDE.md 컨벤션 위반: 멀티라인 JSDoc 주석 (Conventions)

파일: apps/blog/web/src/app/posts/[...slug]/PostClient.tsx:1406–1442

CLAUDE.md 규칙:

"Never write multi-paragraph docstrings or multi-line comment blocks — one short line max."

BLOCK_MARKDOWN_COMPONENTSisBlockMarkdownChild에 각각 7줄, 8줄 JSDoc 블록이 추가되었습니다. isBlockMarkdownChild는 함수명과 구현 로직이 충분히 자명하므로 각각 한 줄 요약으로 대체하거나, 특히 주의해야 할 code 제외 이유만 인라인 주석 한 줄로 남기면 됩니다.


요약

코드 수정의 핵심 로직(인라인 code 제외, node: _node 파괴 대입으로 Hast 노드가 DOM에 전파되지 않도록)은 정확하고 잘 설계되었습니다. CodeBlock이 이미 node: _node로 내부 처리하고 있어 code(props) { return <CodeBlock {...props} />; } 패턴도 안전합니다. 블로그 글의 scheduledDate 설정도 CLAUDE.md 요건에 부합합니다. 위 두 가지는 머지를 막는 버그가 아니라 정리 기회입니다.

함수명이 이미 전달하는 내용을 반복하던 JSDoc 제거(CLAUDE.md: 기본은
주석 없음, non-obvious WHY만). hydration mismatch 방지 의도와 identity/
공개-prop 식별 이유만 인라인 주석으로 유지.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
isBlockMarkdownChild를 PostClient에서 markdownBlocks.ts로 분리하고
Vitest 테스트(14 케이스)를 co-located로 추가. 코드 리뷰 봇이 반복 지적한
"p 오버라이드 테스트 부재"를 해소하고, 그간 회귀가 잦던 분기를 잠근다:

- 직접 매핑된 블록 컴포넌트(Callout/Figure/FileTree) identity 판정
- 이미지: 인라인 래퍼라 identity 불가 → src 공개 prop으로 판정
- inline vs fenced code: 동일 핸들러라 language-* className으로만 구분
- phrasing 네이티브 요소/문자열/숫자/null은 비블록

src/의 React 의존 테스트는 vitest 러너가 담당(node --test 글롭 밖)이라
별도 설정 변경 없이 편입된다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Han5991 Han5991 merged commit 9dcc5af into main Jun 27, 2026
4 of 6 checks passed
@Han5991 Han5991 deleted the feat/typescript-update branch June 27, 2026 17:27
@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review for PR 129

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review summary: PR does three things: TypeScript 6 blog post major reorg, inline code block element misclassification rendering bug fix, .idea/ cleanup + admin-posts-index.json gitignore. Issue [Medium]: BLOCK_MARKDOWN_COMPONENTS allowlist maintenance burden - when adding new block-level custom components to the components map, forgetting to update BLOCK_MARKDOWN_COMPONENTS will silently reintroduce invalid nesting (p>div). Issue [Low]: src prop detection is somewhat broad - audio/video elements via rehype-raw could be misclassified as block. Well done: node:_node separation, admin-posts-index.json gitignore, scheduled publish settings accurate. Generated with Claude Code

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review

This PR improves an inline isBlockElement heuristic into a tested, extracted markdownBlocks.ts module, and correctly fixes the reported bug where inline backtick code was misidentified as a block element. The .idea/ cleanup and admin-posts-index.json gitignore change are both correct (the index is generated by generate-search-index.ts during prebuild). Overall the change is well-structured and the test coverage is appropriate.

Three findings worth addressing:


Finding 1 (maintenance hazard): BLOCK_MARKDOWN_COMPONENTS requires manual sync with PostClient

apps/blog/web/src/app/posts/[...slug]/markdownBlocks.ts, line 8

The Set is a manually-maintained parallel of the components map in PostClient.tsx. If a new block-rendering component is added to PostClient without also being added to the Set, isBlockMarkdownChild silently returns false for it, reintroducing invalid <p><div> nesting and hydration mismatches. No TypeScript guard, lint rule, or test catches this gap.

Suggestion: define a shared BLOCK_COMPONENTS constant co-located with the components map and import it in markdownBlocks.ts, so there is one source of truth. Or export a BLOCK marker from each component and derive the Set automatically.


Finding 2 (minor): img without src not detected as block

apps/blog/web/src/app/posts/[...slug]/markdownBlocks.ts, line 23

typeof src === 'string' is false when src is undefined. A raw <img alt='x'> (no src) embedded via rehypeRaw inside a paragraph is classified as inline, while MarkdownImage may still render a block-level Zoom wrapper. The test at line 44 deliberately encodes this as false, suggesting it was intentional — but worth confirming whether MarkdownImage safely falls back to inline rendering when src is missing.


Finding 3 (low): img handler missing node: _node suppression

apps/blog/web/src/app/posts/[...slug]/PostClient.tsx (img handler)

This PR adds node: _node destructuring to p, table, and li handlers to prevent the hast node from leaking into DOM props. The img handler does not follow the same pattern. Currently safe since it destructures only src and alt, but a future edit adding ...props would forward the hast node to the DOM element. Applying the convention uniformly closes this footgun.


Summary: Finding 1 is the most actionable. The sync hazard between BLOCK_MARKDOWN_COMPONENTS and the PostClient components map is the most likely future defect as new block components are added. Findings 2 and 3 are low-severity.

Generated with Claude Code

@claude

claude Bot commented Jun 27, 2026

Copy link
Copy Markdown

Code Review

PR은 세 가지 일을 합니다: TypeScript 6 삽질기 블로그 글 대폭 정비, 인라인 code<p> 안에서 블록 요소로 오판되던 렌더링 버그 수정, .idea/ 파일 정리 + admin-posts-index.json gitignore 추가. 블로그 글의 내용 보강(PR diff 추적, 섹션 재구성, 결론 보강)은 품질 면에서 잘 됐고, 예약 발행 설정(status: scheduled, scheduledDate: 2026-07-02T09:00:00+09:00)도 isPostVisible() 로직과 정확히 맞습니다.


발견된 이슈

[중간] PostClient.tsxBLOCK_MARKDOWN_COMPONENTS 허용 목록의 유지보수 부담

isBlockMarkdownChild는 블록 컴포넌트를 자동으로 탐지하던 기존 방식(typeof node.type !== 'string')에서 명시적 허용 목록(BLOCK_MARKDOWN_COMPONENTS Set + src/className 휴리스틱)으로 전환했습니다. 인라인 code를 잘못 블록으로 분류하던 버그를 정확히 잡아낸 설계이지만, 새로운 블록 수준 커스텀 컴포넌트를 components 맵에 추가할 때 BLOCK_MARKDOWN_COMPONENTS도 함께 갱신하지 않으면 무효 중첩(<p><div>)이 조용히 다시 생깁니다. Set과 컴포넌트 맵이 서로 다른 위치에 있어 유지보수 연결고리가 약합니다.

두 곳의 동기화를 강제하는 가장 간단한 방법은 BLOCK_MARKDOWN_COMPONENTS를 컴포넌트 맵 정의 바로 옆에 두거나, 컴포넌트 맵 자체를 상수로 분리해 Set을 그것에서 파생시키는 것입니다.


[낮음] src prop 감지가 다소 넓습니다

isBlockMarkdownChild 안의 if (typeof src === 'string') return true 체크는 현재 components 맵에서 src를 받는 것이 MarkdownImage뿐이라 실제 문제는 없습니다. 하지만 rehype-raw를 통해 <audio src="..."><video src="..."><p> 안에 들어오면 블록으로 잘못 판정됩니다. HTML5에서 <audio><p> 안에 쓸 수 있는 인라인 요소지만, 이 경우 <div>로 승격되어 스타일링 차이가 생깁니다. 지금 당장 문제가 되는 경로는 없으나 확인 의미로 남겨둡니다.


잘 된 부분

  • node: _node 분리p, table, li 핸들러에서 react-markdown의 내부 HAST 노드(node)를 ...props에서 빼 DOM으로 전달되지 않도록 한 것은 올바른 수정입니다. React Unknown prop 경고를 제거하는 정확한 방법입니다.
  • admin-posts-index.json gitignore — 빌드 시 generate-search-index.ts가 해당 파일을 생성하고 SSG 후 out/에 포함됩니다. draft/scheduled 포스트 메타데이터가 포함되기 때문에 git 추적 제외가 맞습니다. CI 빌드 흐름은 정상입니다.
  • 예약 발행 설정status: scheduled + scheduledDate: 2026-07-02T09:00:00+09:00isPostVisible()에서 오늘(2026-06-27)에 대해 정확히 false를 반환합니다.

전반적으로 버그 수정의 의도는 명확하고 핵심 케이스(인라인 code)는 잘 해결됐습니다. BLOCK_MARKDOWN_COMPONENTS 동기화 부담에 대한 코멘트 혹은 구조적 보완을 고려해보면 좋을 것 같습니다.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant