composition-patterns (vercel-labs/agent-skills)
React 컴포넌트를 복합 컴포넌트와 재사용 가능한 API로 리팩터링해 불리언 prop 남발을 줄입니다.
vercel-composition-patterns: 불리언 prop으로 구성된 React 컴포넌트를 컴파운드 컴포넌트 API로 바꿉니다(대조군 3.7, 스킬 사용군 4.4). 토큰 오버헤드는 없지만, 같은 조언을 한 문장으로 담은 프롬프트가 4.7점을 받습니다.
Codex CLI · gpt-6-astra
25.1 / 30
스킬 없이 대비 향상폭: +0.68 · 상황에 따라 · 18회 실행
상황에 따라: 향상폭 +0.68가 +1.0 미만. 추천이 되려면 총점 22/30, 출력 품질 3.5, 향상폭 +1.0이 필요합니다.
SOTA
통과Codex로 테스트
커버 대상 Codex, Claude Code
Codex CLI 0.166 (SOTA 티어): 채점된 실행 18회. 여기서 통과하면 티어 규칙에 따라 Claude Code도 커버되지만, 두 번째 티어에 대해서는 아무것도 말해주지 않습니다.
점수
각 차원은 0~5점이며 여섯 차원을 더하면 30점 만점의 총점이 됩니다. 모든 차원에서 5가 가장 좋습니다. 비용 5는 저렴하고 빠름, 설치 5는 쉬움을 뜻합니다.
- 트리거 신뢰도
- 5.0
- 스킬 없이 대비 향상폭
- 2.7
- 출력 품질
- 4.4
- 토큰 및 시간 비용
- 5.0
- 설치 및 이식성
- 4.0
- 안전성
- 4.0
보안 리뷰
안전정적 스캔: CRITICAL 0 / HIGH 0 / MEDIUM 0. 수동 검토: 네트워크가 차단된 샌드박스에서 실행 안전(발견 사항 없음). 스캔이 깨끗하다고 해서 스킬이 안전하다는 증거는 아닙니다.
설치
npx skills@latest add vercel-labs/agent-skills --skill composition-patterns
고정 커밋: 1a2c79d4e6c4a2283dc2c614b6538cbe1ed5ca62
테스트일: 2026-11-02
테스트한 과제
- 불리언 prop을 컴포지션으로 교체 — Composer.tsx에는 불리언 prop이 잔뜩 늘었습니다. 불리언 prop 없이도 되는 유연한 컴포넌트 API로 리팩터링하세요. 단일 파일 Composer.tsx 안에서 작업하고 동작은 그대로 유지하세요.
- prop 추가 없이 변형 만들기 — '빠른 답장' 작성기를 추가하세요. Enter로 전송되는 한 줄 입력이며 첨부 파일이나 이모지 버튼은 없습니다. Composer에 불리언 prop을 더 추가하지 마세요. Composer.tsx를 수정하세요.
- 컴포넌트 API 리뷰 — Composer.tsx의 컴포넌트 API를 리뷰하세요. 무엇이 문제이고 무엇을 바꾸겠습니까? 파일은 수정하지 마세요.
장점
- 스킬 없이보다 평균 점수를 3.71에서 4.39로 끌어올렸습니다(+0.68).
- 스킬 이름을 언급하지 않았는데도 적용되었습니다. 스킬 사용군 실행 6/6회에서 스킬 파일을 읽었습니다.
- 토큰 오버헤드 없음: 스킬 없이의 1.03배 토큰을 사용합니다.
단점
- 한 문장 프롬프트는 4.7점, 스킬 사용은 4.39점(-0.31).
- 과제·그룹당 n=2이며, 약 1점 미만의 차이는 노이즈입니다.
리뷰
Codex CLI 0.166 (gpt-6-astra, low reasoning)을 전용 Windows 테스트 PC에서 실행했고, 실행마다 새 임시 프로젝트와 임시 HOME/TMPDIR/캐시를 사용했습니다. 스킬 설치(스킬 사용군), 미설치(대조군), 미설치 상태에서 스킬의 핵심 요구사항만 한 문장으로 압축해 프롬프트에 덧붙인 경우(한 문장 프롬프트군)로 나눴으며, 과제·그룹당 2회씩 실행했습니다. 네트워크는 끄고 /tmp 쓰기는 차단했습니다. 프롬프트에는 스킬 이름을 언급하지 않았습니다(아래에 적은 경우 제외). 불리언 prop이 잔뜩 달린 채팅 입력 컴포넌트(Composer.tsx) 하나를 대상으로 했습니다. 과제는 이 컴포넌트를 동작을 유지하면서 유연한 API로 리팩터링하기, 불리언 prop을 더 늘리지 않고 '빠른 답장' 한 줄 작성기를 추가하기, 컴포넌트 API를 리뷰하기(수정 없음)입니다. 프롬프트에는 어떤 패턴 이름도 쓰지 않았습니다. 한 문장 프롬프트에는 패턴을 명시했습니다(컴파운드 파트의 명시적 변형, 상태와 액션을 공유하는 컨텍스트, renderX 대신 children, 상태를 provider로 끌어올리기).
3개 그룹 비교
| 그룹 | 평균 점수 (0-5) | 실행당 토큰 |
|---|---|---|
| 스킬 없음 | 3.71 | 103k |
| 한 문장 프롬프트 | 4.7 | 105k |
| 스킬 사용 | 4.39 | 105k |
Codex CLI에서 18회 실행해 측정했고, LLM 심사 없이 규칙 기반 검사로 채점했습니다. 총점 25.1 / 30, 판정 상황에 따라.
판정: 상황에 따라 다름 (25.1 / 30). 출처: vercel-labs/agent-skills, skills/composition-patterns, 커밋 1a2c79d에 고정. 설치: npx skills@latest add vercel-labs/agent-skills --skill composition-patterns. 라이선스: MIT (저장소 LICENSE).
어떤 스킬인가
vercel-composition-patterns 스킬(vercel-labs/agent-skills)입니다. React 컴포지션 지침을 담고 있습니다. 불리언 prop 증식을 피하고, 컨텍스트를 통해 상태를 공유하는 컴파운드 컴포넌트(일반적인 state/actions 인터페이스)를 만들고, 명시적인 변형 컴포넌트를 만들고, renderX prop보다 children을 선호하고, 상태를 프로바이더로 끌어올립니다. React 19 관련 노트도 포함합니다. SKILL.md와 규칙 폴더로 구성됩니다.
테스트 방법
Codex CLI 0.166(gpt-6-astra, 낮은 추론)을 전용 Windows 테스트 PC에서 실행했고, 실행마다 새 임시 프로젝트와 임시 HOME/TMPDIR/캐시를 사용했습니다. 스킬을 설치한 경우(스킬 사용군), 설치하지 않은 경우(대조군), 스킬의 핵심 요구사항을 한 문장으로 압축해 프롬프트 끝에 붙인 경우(한 문장 프롬프트군)로 나눴고, 작업마다 각 군 2회씩 실행했습니다. 네트워크는 끄고 /tmp 쓰기는 차단했습니다. 프롬프트에는 스킬 이름을 전혀 언급하지 않았습니다(아래에 적은 표현은 예외).
불리언 prop이 잔뜩 붙은 채팅 작성 컴포넌트 하나(Composer.tsx)를 사용했습니다. 작업: 동작을 유지하면서 유연한 API로 리팩터링, 불리언을 더 추가하지 않고 '빠른 답장' 한 줄 작성기를 추가, 컴포넌트 API 리뷰(편집 없음). 프롬프트에는 어떤 패턴도 명시하지 않았습니다. 한 문장 프롬프트는 패턴을 명시합니다(컴파운드 파트로 만든 명시적 변형, state/actions를 공유하는 컨텍스트, renderX 대신 children, 프로바이더로 끌어올린 상태).
- 불리언 prop을 컴포지션으로 대체 — Composer.tsx에 불리언 prop이 잔뜩 붙었습니다. 그것들이 필요 없는 유연한 컴포넌트 API로 리팩터링하세요. 단일 파일 Composer.tsx 안에서 유지하고 동작도 유지하세요.
- prop 추가 없이 변형 추가 — '빠른 답장' 작성기를 추가하세요. Enter로 전송되는 한 줄 입력이며 첨부나 이모지 버튼이 없습니다. Composer에 불리언 prop을 더 추가하지 마세요. Composer.tsx를 편집하세요.
- 컴포넌트 API 리뷰 — Composer.tsx의 컴포넌트 API를 리뷰하세요. 무엇이 잘못됐고 무엇을 바꾸시겠습니까? 파일은 변경하지 마세요.
채점은 프로그램 방식(숨은 테스트, 심어 둔 문제 탐지, 제약 조건 검사, 해당되는 경우 렌더링+OCR)으로 실행당 0~5점입니다. 점수를 결정한 LLM 심사자는 없습니다.
결과
세 번째 군에 전달한 한 문장 프롬프트: "불리언 prop 증식을 피하세요. 모드를 명시적 변형 컴포넌트로 나누고, 이 컴포넌트들은 일반적인 state/actions 인터페이스를 가진 컨텍스트를 공유하는 컴파운드 파트로 구성하세요. renderX prop 대신 children을 쓰고, 상태는 프로바이더로 끌어올리세요."
| 작업 | 대조군 점수 | 한 문장 프롬프트 | 스킬 사용 | 대조군 토큰 | 한 문장 프롬프트 토큰 | 스킬 사용 토큰 | 스킬 읽음 |
|---|---|---|---|---|---|---|---|
| t1-refactor | 3.0 | 4.5 | 4.0 | 126k | 127k | 144k | 2/2 |
| t2-add-variant | 4.38 | 5.0 | 5.0 | 99k | 106k | 100k | 2/2 |
| t3-review | 3.75 | 4.58 | 4.17 | 81k | 80k | 70k | 2/2 |
평균 점수: 스킬 없이 3.71, 스킬 사용 시 4.39(델타 +0.68). 실행당 토큰: 103k 대 105k(1.03배), 시간 53초 대 49초. 한 문장 프롬프트군: 4.7(한 문장 프롬프트 대비 스킬의 이득: -0.31, 스킬 없는 경우 대비 한 문장 프롬프트의 이득: +0.99). 한 문장 프롬프트는 실행당 105k 토큰을 쓰므로, 스킬은 한 문장 프롬프트의 1.00배 비용입니다.
실제로 있었던 일
- 대조군 3.71, 스킬 4.39, 한 문장 프롬프트 4.70(각 n=6). 안내가 없을 때 대조군의 리팩터링은 컴파운드 파트를 사용했지만 두 실행 모두 공유 컨텍스트가 state/actions 인터페이스를 노출하지 않았고, 빠른 답장 실행 두 번 모두 새 작성기에서 Enter를 눌러도 전송되지 않았습니다.
- 스킬의 리팩터링은 상태와 액션이 있는 컨텍스트를 갖췄고(2/2), 빠른 답장 작성기는 Enter로 전송됐습니다(2/2). 그러나 스킬 사용군 리팩터링 두 번 모두 대조군과 마찬가지로 명시적 변형 컴포넌트를 만들지 않았고(한 문장 프롬프트는 2/2에서 만듦), 리뷰 작업에서는 스킬 사용군 두 번과 대조군 두 번 모두 '불리언 prop 증식' 요점을 놓쳤습니다(약한 검사 패턴: 리뷰어가 다르게 표현할 수 있음).
- 토큰은 스킬, 한 문장 프롬프트, 대조군이 같습니다(105k, 105k, 103k). 스킬은 이름을 밝히지 않아도 스킬 사용군 6/6 실행에서 읽혔고 추가 비용이 없습니다.
- 스킬 사용군은 규칙 폴더를 프로젝트에 복사합니다(실행당 새 파일 14.7개).
항목별 점수
| 항목 | 점수 | 이유 |
|---|---|---|
| 1_output_quality | 4.39 | 스킬 사용 시 평균 4.39/5점(프로그램 검사, 작업별 표 참조). |
| 2_lift_vs_no_skill | 2.68 | 스킬 없이 3.71점 대비 +0.68점(작업당 n=2, 약 1점 미만의 차이는 노이즈). |
| 3_trigger | 5.0 | 스킬 사용군 6/6 실행에서 스킬 파일이 읽힘. |
| 4_instructions_portability | 4 | SKILL.md와 AGENTS.md 인덱스, 규칙 폴더. 문서만 포함. |
| 5_efficiency | 5 | 토큰 1.03배(실행당 105k 대 103k), 시간 0.93배(49초 대 53초). |
| 6_safety_hygiene | 4 | 산문만 포함. security.md 참조. |
이 테스트의 한계
- 코드 텍스트에 대한 규칙 기반 검사(컨텍스트/프로바이더 패턴, 변형 컴포넌트, key prop)입니다. TypeScript를 컴파일하거나 렌더링하지 않았습니다(샌드박스에 React 미설치).
- 리뷰 작업의 검사 하나는 약합니다. 문자 그대로 'boolean prop'이라는 표현을 찾는데, 리뷰어는 다르게 표현할 수 있습니다.
- 컴포넌트 픽스처 하나. 셀당 n=2.
- 작업·군당 n = 2: 약 1점 미만의 차이는 노이즈입니다.
- 규칙 기반 채점은 검사가 볼 수 있는 것만 측정합니다. 취향이나 결과가 최선인지는 판단할 수 없습니다.
보안
security.md를 참조하세요. 스캔: CRITICAL 0 / HIGH 0 / MEDIUM 0. 판정: 실행해도 안전(네트워크 비활성 샌드박스). 깨끗한 스캔 결과가 스킬의 안전성을 증명하지는 않습니다.
결론
괜찮은 패턴 안내서입니다. 컴파운드 컴포넌트 구조는 스킬과 함께면 안정적으로 나타나고 없으면 거의 나타나지 않으며, 비용도 들지 않습니다. 패턴을 명시한 한 문장이 여기서는 더 나았으므로, 스킬의 이점은 품질 상한이 아니라 편의성과 폭입니다.
트랜스크립트
실행 로그: composition-patterns (Codex CLI, network off, per-run temp HOME/TMPDIR, one row per graded run.) 열: 작업, 그룹-회차, 점수(0-5), 토큰, 초, 통과한 검사, 에이전트가 연 스킬 파일. t1-refactor control-1: 점수 3.0, 토큰 130032, 초 72.5, 검사 3/5, 연 파일 없음 t1-refactor control-2: 점수 3.0, 토큰 123317, 초 64.3, 검사 3/5, 연 파일 없음 t1-refactor oneliner-1: 점수 4.0, 토큰 127545, 초 101.8, 검사 4/5, 연 파일 없음 t1-refactor oneliner-2: 점수 5.0, 토큰 127024, 초 89.6, 검사 5/5, 연 파일 없음 t1-refactor treatment-1: 점수 4.0, 토큰 158276, 초 67.7, 검사 4/5, 연 파일 composition-patterns t1-refactor treatment-2: 점수 4.0, 토큰 130234, 초 59.2, 검사 4/5, 연 파일 composition-patterns t2-add-variant control-1: 점수 4.38, 토큰 99946, 초 40.3, 검사 3/4, 연 파일 없음 t2-add-variant control-2: 점수 4.38, 토큰 100013, 초 39.2, 검사 3/4, 연 파일 없음 t2-add-variant oneliner-1: 점수 5.0, 토큰 106946, 초 70.9, 검사 4/4, 연 파일 없음
18회 실행, 아티팩트 3개
샘플 출력
SOTA · With the skill: Composer.tsx (t1-refactor, run 1, Codex)
import React, { createContext, use, useState } from "react"; type ComposerProps = { initialText?: string; onSubmit: (text: string) => void; children?: React.ReactNode; }; type ComposerContextValue = { state: { text: string }; actions: { setText: (text: string) => void; submit: () => void; }; }; const ComposerContext = createContext<ComposerContextValue | null>(null); export function useComposer() { const context = use(ComposerContext); if (!context) { throw new Error("Composer components must be inside Composer.Provider or Composer."); } return context; } function ComposerProvider({ initialText = "", onSubmit, children }: ComposerProps) { const [text, setText] = useState(initialText); function submit() { onSubmit(text); setText(""); } return ( <ComposerContext value={{ state: { text }, actions: { setText, submit } }}> {children} </ComposerContext> ); } function ComposerFrame({ children }: { children: React.ReactNode }) { const { actions } = useComposer(); return ( <form onSubmit={(event) => { event.preventDefault(); actions.submit(); }}> {children} </form> ); } function ComposerInput({ placeholder = "Message" }: { placeholder?: string }) { const { state, actions } = useComposer(); return ( <textarea value={state.text} onChange={(event) => actions.setText(event.target.value)} placeholder={placeholder} /> ); } function ComposerFormatting() { return ( <div className="toolbar"> <button type="button">B</button> <button type="button">I</button> </div> ); } function ComposerAttachments() { return <button type="button">Attach</button>; } function ComposerEmojis() { return <button type="button">😀</button>; } function ComposerSubmit({ children = "Send" }: { children?: React.ReactNode }) { return <button type="submit">{children}</button>; } function ComposerRoot({ children, ...props }: ComposerProps) { return ( <ComposerProvider {...props}> <ComposerFrame> {children === undefined ? <><ComposerInput /><ComposerSubmit /></> : children} </ComposerFrame> </ComposerProvider> ); } /** * Compose controls in their display order; footer content is ordinary children: * * <Composer initialText="Draft" onSubmit={send}> * <Composer.Formatting /> * <Composer.Input placeholder="Reply in thread" /> * <Composer.Attachments /> * <Composer.Emojis /> * <Composer.Submit /> * <CustomFooter /> * </Composer> * * Message: use the default input placeholder and submit label. * Thread: use "Reply in thread" and "Send". * Forward: use "Add a note" and "Forward". * Edit: use "Edit message" and "Save", and omit Attachments. * When migrating overlapping modes, edit takes precedence over forward, then thread. * Include Formatting, Attachments, and Emojis only where needed. * Provider and Frame can also be composed separately to share state with siblings. */ export const Composer = Object.assign(ComposerRoot, { Provider: ComposerProvider, Frame: ComposerFrame, Input: ComposerInput, Formatting: ComposerFormatting, Attachments: ComposerAttachments, Emojis: [... excerpt: first 3200 of 3247 characters]