commit 7e99b9e13d86de795213fe183059539bbd23a64a Author: Zhongwei Li Date: Sun Nov 30 08:31:31 2025 +0800 Initial commit diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a4b08ab --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,12 @@ +{ + "name": "code-skills", + "description": "code skills with claude", + "version": "1.0.0", + "author": { + "name": "hynu", + "email": "khw1031@gmail.com" + }, + "skills": [ + "./skills" + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca5ee30 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# code-skills + +code skills with claude diff --git a/plugin.lock.json b/plugin.lock.json new file mode 100644 index 0000000..ef51d42 --- /dev/null +++ b/plugin.lock.json @@ -0,0 +1,49 @@ +{ + "$schema": "internal://schemas/plugin.lock.v1.json", + "pluginId": "gh:khw1031/claude-marketplace:plugins/code-skills", + "normalized": { + "repo": null, + "ref": "refs/tags/v20251128.0", + "commit": "737a96141b7e3e614db81873f0315085e7e9d7a6", + "treeHash": "251fc79965b5e16602a05aecce7853a5eb752bc5a1e642915967c2e419042314", + "generatedAt": "2025-11-28T10:19:29.947694Z", + "toolVersion": "publish_plugins.py@0.2.0" + }, + "origin": { + "remote": "git@github.com:zhongweili/42plugin-data.git", + "branch": "master", + "commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390", + "repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data" + }, + "manifest": { + "name": "code-skills", + "description": "code skills with claude", + "version": "1.0.0" + }, + "content": { + "files": [ + { + "path": "README.md", + "sha256": "546ac5a4ece241a3848a5d028340250bb2f1b4671c41a538668670d78d2cc67f" + }, + { + "path": ".claude-plugin/plugin.json", + "sha256": "8411918a1753761fb3d02c1ea218d34e6761afc4e9efae71fa66d3f3a01d02fb" + }, + { + "path": "skills/review-react/SKILL.md", + "sha256": "4cc5f92f0abc2929b8cc302e39f739d343d9e7f86badafe4c74ff0a83bd9964c" + }, + { + "path": "skills/review-react/references/typescript.md", + "sha256": "c7e375937c423fa3223aa01ac01f59b2e47d29ced34fd222395dace5b7785aef" + } + ], + "dirSha256": "251fc79965b5e16602a05aecce7853a5eb752bc5a1e642915967c2e419042314" + }, + "security": { + "scannedAt": null, + "scannerVersion": null, + "flags": [] + } +} \ No newline at end of file diff --git a/skills/review-react/SKILL.md b/skills/review-react/SKILL.md new file mode 100644 index 0000000..1f4fa95 --- /dev/null +++ b/skills/review-react/SKILL.md @@ -0,0 +1,190 @@ +--- +name: review-react +description: Expert-level frontend code review specialist for production-grade TypeScript/React applications. Use this skill when reviewing pull requests, performing code audits, or analyzing frontend codebases for type safety, performance, security, and maintainability issues. Focuses on React/TypeScript stack with emphasis on runtime safety and production readiness. +--- + +# Frontend Code Review Expert + +당신은 10년 이상의 실무 경험을 보유한 시니어 프론트엔드 개발자입니다. TypeScript, React, 성능 최적화, 접근성, 보안에 대한 깊은 이해를 바탕으로 코드 리뷰를 수행합니다. + +## 핵심 검토 우선순위 + +1. **타입 안전성** - 런타임 에러 방지가 최우선 +2. **코드 아키텍처** - 유지보수성과 확장성 +3. **성능 최적화** - 불필요한 리렌더링 방지 +4. **보안 취약점** - XSS, 주입 공격 방지 +5. **접근성** - WCAG 2.1 AA 준수 + +## 필수 참조 문서 + +코드 리뷰 수행 전 다음 문서들을 반드시 확인하세요: + +- `./references/typescript.md` - TypeScript 코딩 규칙 및 팀 컨벤션 +- `./references/react-patterns.md` - React 패턴 및 베스트 프랙티스 (존재 시) +- `./references/security-guidelines.md` - 보안 체크리스트 (존재 시) + +**중요**: 참조 문서가 없는 경우, 업계 표준 베스트 프랙티스를 적용하되 반드시 그 사실을 리뷰 결과에 명시하세요. + +## 리뷰 프로세스 + +### 1단계: 구조 분석 (먼저 실행) + +변경된 파일들의 전체 구조를 파악: + +- 파일/모듈 구조의 적절성 +- 관심사의 분리 (Separation of Concerns) +- 의존성 관리 및 순환 참조 여부 + +### 2단계: 타입 안전성 검증 + +**Critical 체크리스트**: + +- [ ] any 타입 사용 여부 - 대안 제시 필수 +- [ ] Non-null assertion 연산자 남용 - 안전한 체크로 대체 +- [ ] 타입 단언(as) 남용 - 타입 가드로 개선 +- [ ] Optional chaining 누락 - 런타임 에러 가능성 +- [ ] Enum vs Union Type 선택 적절성 + +참조: `./references/typescript.md`의 타입 정의 규칙 준수 여부 + +### 3단계: React 패턴 분석 + +**검토 항목**: + +- 불필요한 리렌더링 (React DevTools Profiler 추천) +- 메모이제이션 필요 여부 (useMemo, useCallback, React.memo) +- Effect 의존성 배열 정확성 - 무한 루프 위험 +- 커스텀 훅 추출 가능성 - 재사용성 +- Props drilling vs Context 사용 적절성 + +### 4단계: 보안 검증 + +**필수 체크**: + +- [ ] XSS 취약점: dangerouslySetInnerHTML 사용 시 sanitization +- [ ] 사용자 입력 검증: Zod/Yup 스키마 적용 여부 +- [ ] 민감 정보 노출: API 키, 토큰 하드코딩 확인 +- [ ] CSRF 방어: 상태 변경 API의 토큰 검증 +- [ ] 안전하지 않은 의존성: npm audit 권장 + +### 5단계: 성능 및 최적화 + +- 번들 사이즈 영향도 (대용량 라이브러리 import) +- 비동기 로직 최적화 (병렬 처리 가능 여부) +- 이미지/에셋 최적화 (lazy loading, WebP 사용) +- Code splitting 적용 가능성 + +## 리뷰 결과 형식 + +각 발견사항은 다음 템플릿을 사용하세요: + +[심각도] 카테고리: 문제 요약 + +위치: 파일명:라인번호 + +현재 코드: +[코드 블록] + +문제점: + +- 참조 문서 규칙 위반 여부 (규칙명 명시) +- 구체적인 문제 설명 +- 발생 가능한 부작용 (런타임 에러, 성능 저하 등) + +개선 방안: +[수정된 코드 예시] + +우선순위: 높음 / 중간 / 낮음 + +**심각도 분류**: + +- 🔴 **Critical**: 즉시 수정 필요 (보안, 치명적 버그, 런타임 에러) +- 🟡 **Warning**: 개선 권장 (성능, 유지보수성, 타입 안전성) +- 🔵 **Suggestion**: 선택적 개선 (코드 스타일, 마이너 최적화) + +## 리뷰 완료 체크리스트 + +리뷰 마무리 전 다음을 확인: + +- [ ] typescript.md 규칙 준수율 산출 +- [ ] Critical 이슈가 있다면 Top 3 우선순위 명시 +- [ ] 각 지적사항에 "왜" 문제인지 명확히 설명 +- [ ] 구체적인 코드 예시 제공 (가능한 경우) +- [ ] 장기 개선 제안 (아키텍처 레벨) 포함 + +## 최종 요약 형식 + +```markdown +## 종합 평가 + +### 전체 평가 + +[코드의 전반적인 품질 평가 - 객관적이고 건설적인 톤] + +### 주요 발견사항 + +- Critical: X건 +- Warning: Y건 +- Suggestion: Z건 + +### 참조 문서 준수율 + +- typescript.md: X% 준수 (위반 항목: [...]) + +### 우선 조치 항목 (Top 3) + +1. [가장 심각한 이슈 - 즉시 수정 필요 이유] +2. [두 번째 우선순위] +3. [세 번째 우선순위] + +### 장기 개선 제안 + +- [아키텍처/패턴 레벨의 개선 방향] +- [팀 전체에 적용 가능한 베스트 프랙티스] + +추가 지침 + +톤 및 스타일 + +- 비판적이되 건설적인 피드백 +- "이렇게 하면 안 됩니다" 대신 "이렇게 하면 더 안전합니다" 표현 +- 주니어 개발자도 이해할 수 있도록 설명 + +컨텍스트 관리 + +- 500줄 이상의 대규모 변경은 파일별로 분할 리뷰 +- 관련 파일들을 그룹핑하여 순차적으로 검토 + +코드 예시 작성 시 + +- 변경 전/후 비교를 명확히 +- 주석으로 핵심 개선 포인트 표시 +- 실행 가능한 코드 제공 (컴파일 에러 없이) + +기술 스택별 특화 규칙 + +React + TypeScript + +- Functional Component 우선, Class Component 지양 +- Props 인터페이스는 컴포넌트와 같은 파일에 정의 +- Generics 활용하여 타입 재사용성 향상 + +상태 관리 + +- Local state vs Global state 선택 기준 명확히 +- Zustand/Jotai: atom 분리 원칙 +- React Query: stale time, cache time 설정 적절성 + +자동화 도구 추천 + +리뷰 완료 후 다음 도구 실행을 권장하세요: + +- eslint --fix - 자동 수정 가능한 이슈 +- prettier --write - 코드 포맷팅 +- tsc --noEmit - 타입 체크 +- npm audit - 보안 취약점 스캔 + +--- + +참고: 이 스킬은 코드 리뷰에 집중합니다. 코드 작성이나 기능 구현이 필요한 경우, +해당 작업을 먼저 완료하고 별도의 리뷰 요청을 하세요. diff --git a/skills/review-react/references/typescript.md b/skills/review-react/references/typescript.md new file mode 100644 index 0000000..e6fc0fd --- /dev/null +++ b/skills/review-react/references/typescript.md @@ -0,0 +1,490 @@ +# default exports + +프레임워크에서 명시적으로 요구하지 않는 한, default exports를 사용하지 마세요. + +```ts +// BAD +export default function myFunction() { + return
Hello
; +} +``` + +```ts +// GOOD +export function myFunction() { + return
Hello
; +} +``` + +Default exports는 가져오는 파일에서 혼란을 야기합니다. + +```ts +// BAD +import myFunction from "./myFunction"; +``` + +```ts +// GOOD +import { myFunction } from "./myFunction"; +``` + +프레임워크가 default export를 요구하는 특정 상황이 있습니다. 예를 들어, Next.js, Expo Router는 페이지 또는 스크린에 대해 default export를 요구합니다. + +```tsx +// This is fine, if required by the framework +export default function MyPage() { + return
Hello
; +} +``` + +# any inside generic functions + +제네릭 함수를 빌드할 때, 함수 본문 내부에서 any를 사용해야 할 수도 있습니다. + +이는 TypeScript가 종종 런타임 로직을 타입 수준 로직과 일치시키지 못하기 때문입니다. + +One example: +```ts +const youSayGoodbyeISayHello = < + TInput extends "hello" | "goodbye", +>( + input: TInput, +): TInput extends "hello" ? "goodbye" : "hello" => { + if (input === "goodbye") { + return "hello"; // Error! + } else { + return "goodbye"; // Error! + } +}; +``` + +타입 레벨(그리고 런타임)에서, 이 함수는 입력이 `hello`일 때 `goodbye`를 반환합니다. +TypeScript에서 이를 간결하게 작동시킬 방법이 없습니다. +따라서 `any`를 사용하는 것이 가장 간결한 해결책입니다: + +```ts +const youSayGoodbyeISayHello = < + TInput extends "hello" | "goodbye", +>( + input: TInput, +): TInput extends "hello" ? "goodbye" : "hello" => { + if (input === "goodbye") { + return "hello" as any; + } else { + return "goodbye" as any; + } +}; +``` + +제네릭 함수 외부에서는 `any`를 극히 드물게 사용하세요. + +# discriminated unions + +데이터가 여러 다른 형태 중 하나일 수 있는 경우를 모델링하기 위해 적극적으로 판별 유니온(discriminated unions)을 사용하세요. + +예를 들어, 환경 간에 이벤트를 전송할 때: +```ts +type UserCreatedEvent = { + type: "user.created"; + data: { id: string; email: string }; +}; + +type UserDeletedEvent = { + type: "user.deleted"; + data: { id: string }; +}; + +type Event = UserCreatedEvent | UserDeletedEvent; +``` + +판별 유니온(discriminated unions)의 결과를 처리하기 위해 switch 문을 사용하세요: +```ts +const handleEvent = (event: Event) => { + switch (event.type) { + case "user.created": + console.log(event.data.email); + break; + case "user.deleted": + console.log(event.data.id); + break; + } +}; +``` + +'선택적 속성들의 모음' 문제를 방지하기 위해 판별 유니온(discriminated unions)을 사용하세요. +예를 들어, 데이터 가져오기 상태를 설명할 때: + +```ts +// BAD - allows impossible states +type FetchingState = { + status: "idle" | "loading" | "success" | "error"; + data?: TData; + error?: Error; +}; + +// GOOD - prevents impossible states +type FetchingState = + | { status: "idle" } + | { status: "loading" } + | { status: "success"; data: TData } + | { status: "error"; error: Error }; +``` + +# enums + +코드베이스에 새로운 enum을 도입하지 마세요. 기존 enum은 유지하세요. +enum과 유사한 동작이 필요한 경우, `as const` 객체를 사용하세요: + +```ts +const backendToFrontendEnum = { + xs: "EXTRA_SMALL", + sm: "SMALL", + md: "MEDIUM", +} as const; + +type LowerCaseEnum = keyof typeof backendToFrontendEnum; // "xs" | "sm" | "md" + +type UpperCaseEnum = + (typeof backendToFrontendEnum)[LowerCaseEnum]; // "EXTRA_SMALL" | "SMALL" | "MEDIUM" +``` + +숫자형 열거형(numeric enums)은 문자열 열거형(string enums)과 다르게 동작한다는 점을 기억하세요. 숫자형 열거형은 역방향 매핑(reverse mapping)을 생성합니다: +```ts +enum Direction { + Up, + Down, + Left, + Right, +} + +const direction = Direction.Up; // 0 +const directionName = Direction[0]; // "Up" +``` + +이는 위의 `Direction` enum이 네 개가 아닌 여덟 개의 키를 가지게 된다는 것을 의미합니다. + +```ts +enum Direction { + Up, + Down, + Left, + Right, +} + +Object.keys(Direction).length; // 8 +``` + +# import type + +타입을 가져올 때는 항상 import type을 사용하세요. +인라인 `import { type ... }` 보다 최상위 레벨 `import type`을 선호하세요. + +```ts +// BAD +import { type User } from "./user"; +``` + +```ts +// GOOD +import type { User } from "./user"; +``` + +이러한 이유는 특정 환경에서는 첫 번째 버전의 import가 제거되지 않기 때문입니다. 따라서 다음과 같이 남게 됩니다: + +```ts +// Before transpilation +import { type User } from "./user"; + +// After transpilation +import "./user"; +``` + +# installing libraries + +라이브러리를 설치할 때, 자신의 학습 데이터에 의존하지 마세요. + +당신의 학습 데이터는 특정 시점에 끊겨 있습니다. 당신은 아마도 JavaScript와 TypeScript 세계의 최신 개발 사항을 모두 알지 못할 것입니다. + +이는 수동으로 버전을 선택하는 대신(`package.json` 파일을 업데이트하는 방식으로), 라이브러리의 최신 버전을 설치하기 위해 스크립트를 사용해야 한다는 것을 의미합니다. + +```bash +# pnpm +pnpm add -D @typescript-eslint/eslint-plugin + +# yarn +yarn add -D @typescript-eslint/eslint-plugin + +# npm +npm install --save-dev @typescript-eslint/eslint-plugin +``` + +이렇게 하면 항상 최신 버전을 사용할 수 있습니다. + +# interface extends + +상속을 모델링할 때는 항상 interface를 선호하세요. +TypeScript에서 `&` 연산자는 성능이 매우 좋지 않습니다. `interface extends`가 불가능한 경우에만 사용하세요. + +```ts +// BAD + +type A = { + a: string; +}; + +type B = { + b: string; +}; + +type C = A & B; +``` + +```ts +// GOOD + +interface A { + a: string; +} + +interface B { + b: string; +} + +interface C extends A, B { + // Additional properties can be added here +} +``` + +# jsdoc comments + +함수와 타입을 주석 처리하기 위해 JSDoc 주석을 사용하세요. +JSDoc 주석에서는 간결하게 작성하고, 함수의 동작이 명확하지 않은 경우에만 JSDoc 주석을 제공하세요. +같은 파일 내의 다른 함수와 타입에 링크하려면 JSDoc 인라인 `@link` 태그를 사용하세요. + +```ts +/** + * Subtracts two numbers + */ +const subtract = (a: number, b: number) => a - b; + +/** + * Does the opposite to {@link subtract} + */ +const add = (a: number, b: number) => a + b; +``` + +# naming conventions + +- 파일 이름에는 kebab-case를 사용하세요 (예: `my-component.ts`) +- 변수와 함수 이름에는 camelCase를 사용하세요 (예: `myVariable`, `myFunction()`) +- 클래스, 타입, 인터페이스에는 UpperCamelCase(PascalCase)를 사용하세요 (예: `MyClass`, `MyInterface`) +- 상수와 enum 값에는 ALL_CAPS를 사용하세요 (예: `MAX_COUNT`, `Color.RED`) +- 제네릭 타입, 함수 또는 클래스 내부에서는 타입 매개변수에 `T` 접두사를 붙이세요 (예: `TKey`, `TValue`) + +```ts +type RecordOfArrays = Record; +``` + +# noUncheckedIndexedAccess + +사용자가 `tsconfig.json`에서 이 규칙을 활성화한 경우, 객체와 배열의 인덱싱은 예상과 다르게 동작할 수 있습니다. + +```ts +const obj: Record = {}; + +// With noUncheckedIndexedAccess, value will +// be `string | undefined` +// Without it, value will be `string` +const value = obj.key; +``` + +```ts +const arr: string[] = []; + +// With noUncheckedIndexedAccess, value will +// be `string | undefined` +// Without it, value will be `string` +const value = arr[0]; +``` + +# optional properties + +선택적 속성(optional properties)은 극히 드물게 사용하세요. 속성이 진정으로 선택적인 경우에만 사용하고, 속성을 전달하지 않아 발생할 수 있는 버그를 고려하세요. + +아래 예시에서는 항상 사용자 ID를 `AuthOptions`에 전달하고자 합니다. 이는 코드베이스의 어딘가에서 이를 전달하는 것을 잊어버리면 함수가 인증되지 않는 상태가 될 수 있기 때문입니다. + +```ts +// BAD +type AuthOptions = { + userId?: string; +}; + +const func = (options: AuthOptions) => { + const userId = options.userId; +}; +``` + +```ts +// GOOD +type AuthOptions = { + userId: string | undefined; +}; + +const func = (options: AuthOptions) => { + const userId = options.userId; +}; +``` + +# readonly properties + +기본적으로 객체 타입에 `readonly` 속성을 사용하세요. 이는 런타임에 우발적인 변경을 방지합니다. +속성이 진정으로 변경 가능한 경우에만 `readonly`를 생략하세요. + +```ts +// BAD +type User = { + id: string; +}; + +const user: User = { + id: "1", +}; + +user.id = "2"; +``` + +```ts +// GOOD +type User = { + readonly id: string; +}; + +const user: User = { + id: "1", +}; + +user.id = "2"; // Error +``` + +# return types + +모듈의 최상위 레벨에서 함수를 선언할 때, 반환 타입을 명시하세요. 이는 미래의 AI 어시스턴트가 함수의 목적을 이해하는 데 도움이 됩니다. + +```ts +const myFunc = (): string => { + return "hello"; +}; +``` + +이에 대한 한 가지 예외는 JSX를 반환하는 컴포넌트입니다. 컴포넌트의 반환 타입을 선언할 필요가 없습니다, 항상 JSX이기 때문입니다. + +```tsx +const MyComponent = () => { + return
Hello
; +}; +``` + +# throwing + +오류를 발생시키는 코드를 구현하기 전에 신중하게 생각하세요. + +발생한 오류가 시스템에서 바람직한 결과를 생성한다면, 그렇게 하세요. 예를 들어, 백엔드 프레임워크의 요청 핸들러 내에서 사용자 정의 오류를 발생시키는 경우가 있습니다. + +그러나 수동 try catch가 필요한 코드의 경우, 대신 결과 타입(result type)을 사용하는 것을 고려하세요: + +```ts +type Result = + | { ok: true; value: T } + | { ok: false; error: E }; +``` + +예를 들어, JSON을 파싱할 때: +```ts +const parseJson = ( + input: string, +): Result => { + try { + return { ok: true, value: JSON.parse(input) }; + } catch (error) { + return { ok: false, error: error as Error }; + } +}; +``` + +이런 방식으로 호출자에서 오류를 처리할 수 있습니다: +```ts +const result = parseJson('{"name": "John"}'); + +if (result.ok) { + console.log(result.value); +} else { + console.error(result.error); +} +``` + +# 타입 네이밍 규칙 + +타입 네이밍은 단수형을 사용해주세요. + +```ts +// ❌ 잘못된 방식 +type Routes = "user" | "admin/user" | "admin"; + +function goToRoute(route: Routes) { // route는 하나인데 Routes는 복수형 + // ... +} + +// 올바른 방식 +type Route = "user" | "admin/user" | "admin"; + +function goToRoute(route: Route) { + // ... +} + +// 배열이 필요한 경우 +type RouteArray = Route[]; +// 또는 +const routes: Route[] = ["user", "admin"]; +``` + +Union 타입도 실제로는 하나의 값만 나타내므로 단수형으로 네이밍하는 것이 올바름 + +변수와 타입의 케이스를 다르게 사용해주세요 + +```ts +// 권장 방식 +type Route = "user" | "admin/user" | "admin"; +const route: Route = "user"; // 변수는 camelCase, 타입은 PascalCase + +// 작동하지만 비추천 +type route = "user" | "admin/user" | "admin"; +const route: route = "user"; // 문법 하이라이팅이 혼란스러워짐 +``` + +문법 하이라이팅과 가독성 향상, TypeScript가 런타임과 타입 레벨을 구분할 수 있음 + +I, T 접두사로 interface/type 구분 금지 + +```ts +// ❌ 구식 Java 관례 - 비추천 +interface IUser { + name: string; +} + +type TOrganization = { + name: string; +} + +// 권장 방식 +interface User { + name: string; +} + +type Organization = { + name: string; +} +``` + +- Java 관례의 잔재 +- 호버로 interface/type 구분 가능 +- 타입을 interface로 바꿀 때 이름도 변경해야 하는 번거로움 \ No newline at end of file