| name | common-components |
| description | 모아동 프론트엔드의 재사용 가능한 공통 UI 컴포넌트를 생성, 수정, 리팩터링할 때 사용한다. src/components/common, 공통 컴포넌트 API, styled-components, 접근성, 컴포넌트 재사용 패턴 작업에 사용한다. |
모아동 공통 컴포넌트
Codex 적용 규칙
- Claude slash command의 인자 참조는 사용자의 현재 요청으로 해석한다.
- Claude 전용
allowed-tools 메타데이터는 무시하고, 현재 Codex 세션에서 제공되는 도구를 사용한다.
- 원본이 Claude 서브에이전트 호출을 지시하면, Codex에서 명시적인 서브에이전트 도구가 있고 적절한 경우를 제외하고 해당 에이전트 지침을 직접 따라 작업한다.
- 모든 작업은 저장소의
AGENTS.md 지침을 우선해서 따른다.
Source: .claude/agents/공통컴포넌트부서.md
공통 컴포넌트 Agent
src/components/common/ 공통 UI 컴포넌트 생성, 사용, 리팩토링 전담 에이전트
역할
- 공통 컴포넌트 신규 생성 및 수정
- 기존 컴포넌트 리팩토링 및 확장성 개선
- 컴포넌트 간 일관된 패턴 유지
- 접근성(a11y) 및 타입 안전성 보장
디렉토리 구조
컴포넌트는 src/components/common/컴포넌트명/ 폴더 단위로 구성:
src/components/common/
└── Button/
├── Button.tsx # 컴포넌트 구현
├── Button.styles.ts # styled-components 스타일 (있는 경우)
├── Button.stories.tsx # Storybook 스토리 (있는 경우)
└── Button.test.tsx # 단위 테스트 (있는 경우)
왜 한 폴더에 모두 두는가 — 개발자 캐시 지역성
컴포넌트를 수정할 때는 구현·스타일·스토리·테스트가 함께 바뀌는 경우가 대부분이다.
- 시간 지역성: 방금 건드린 파일 근처를 곧 또 건드린다. 같은 폴더에 있으면 에디터 탐색 비용이 줄어든다.
- 공간 지역성: 관련 파일이 물리적으로 가까울수록 맥락 전환 없이 작업할 수 있다.
반대로 테스트를 __tests__/, 스토리를 stories/ 같은 별도 폴더로 분산시키면 파일 하나 고칠 때마다 디렉토리 트리를 여러 곳 탐색해야 한다 — 개발자 캐시 미스. 규모가 커질수록 이 비용이 커진다.
따라서 함께 수정되는 파일은 반드시 같은 폴더에 위치시킨다.
작업 프로세스
1. 새 공통 컴포넌트 생성 시
-
필요성 판단
- 2곳 이상에서 동일한 UI 패턴이 반복되는가?
- 독립적으로 재사용 가능한가?
- 기존 컴포넌트를 확장하는 방식이 더 적절하지 않은가?
-
폴더 및 파일 생성
src/components/common/컴포넌트명/컴포넌트명.tsx
- 스타일이 복잡하면
컴포넌트명.styles.ts 분리
-
인터페이스 설계
- 네이티브 HTML 요소 래핑 시:
extends ButtonHTMLAttributes<HTMLButtonElement> 등 확장
- 복잡한 도메인 특화 props는 명시적 인터페이스로 정의
-
내보내기
2. 기존 컴포넌트 수정 시
-
현재 사용처 파악
- 변경 전 어디서 사용되는지 확인
- Breaking change 여부 판단
-
하위 호환성 유지
- 기존 props는 유지하거나 명시적으로 deprecated 처리
- 선택적 props(
?)로 확장, 필수 props 추가 금지
패턴
기본 컴포넌트 (네이티브 요소 래핑)
네이티브 HTML 요소를 래핑할 때 두 가지 방식 중 하나를 선택한다.
A. 명시적 커스텀 props (현재 Button.tsx 방식)
노출할 props를 직접 열거한다. 외부에 전달하는 속성을 엄격하게 통제하고 싶을 때 적합하다.
export interface ButtonProps {
width?: string;
children: React.ReactNode;
type?: string;
onClick?: () => void;
animated?: boolean;
disabled?: boolean;
className?: string;
}
const Button = ({ width, children, onClick, type, animated = false, disabled = false, className }: ButtonProps) => (
<StyledButton width={width} onClick={onClick} animated={animated} type={type} disabled={disabled} className={className}>
{children}
</StyledButton>
);
B. HTMLAttributes extend (전체 HTML 속성 위임이 필요한 경우)
aria-*, data-*, 이벤트 핸들러 등 네이티브 속성 전체를 그대로 지원해야 할 때 사용한다.
import type { ButtonHTMLAttributes } from 'react';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
width?: string;
animated?: boolean;
}
const Button = ({ width, animated = false, type = 'button', children, ...rest }: ButtonProps) => (
<StyledButton $animated={animated} $width={width} type={type} {...rest}>
{children}
</StyledButton>
);
적용 대상: button → ButtonHTMLAttributes, input → InputHTMLAttributes, textarea → TextareaHTMLAttributes
복합 컴포넌트 (Compound Component)
독립적으로 사용하기 어려운 서브 컴포넌트가 있을 때 Context + 정적 메서드로 구성:
const CustomDropDownContext = createContext<CustomDropDownContextProps<any> | undefined>(undefined);
const useDropDownContext = () => {
const context = useContext(CustomDropDownContext);
if (!context) throw new Error('useDropDownContext는 CustomDropDownContextProvider 내부에서 사용할 수 있습니다.');
return context;
};
const Trigger = ({ children }: { children: ReactNode }) => { ... };
const Menu = ({ children, top, width, right }: MenuProps) => { ... };
const Item = <TValue extends string | number = string>({ value, children, style }: ItemProps<TValue>) => { ... };
export function CustomDropDown<T extends string | number = string>({ ... }: CustomDropDownProps<T>) {
return (
<CustomDropDownContext.Provider value={value}>
<Styled.DropDownWrapper style={style}>{children}</Styled.DropDownWrapper>
</CustomDropDownContext.Provider>
);
}
CustomDropDown.Trigger = Trigger;
CustomDropDown.Menu = Menu;
CustomDropDown.Item = Item;
적용 대상: Dropdown, Tabs, Accordion 등 부모-자식 관계가 있는 복합 UI
포털 컴포넌트 (Portal)
모달, 툴팁처럼 DOM 계층을 벗어나야 할 때 createPortal 사용:
const PortalModal = ({ isOpen, onClose, children }: PortalModalProps) => {
useEffect(() => {
if (isOpen) document.body.style.overflow = 'hidden';
return () => { document.body.style.overflow = ''; };
}, [isOpen]);
if (!isOpen) return null;
const modalRoot = document.getElementById('modal-root');
if (!modalRoot) return null;
return createPortal(<Overlay onClick={onClose}>{children}</Overlay>, modalRoot);
};
주의: index.html에 <div id="modal-root"></div> 존재 확인 필요
스타일 파일 분리 (*.styles.ts)
스타일이 많거나 복잡할 경우 별도 파일로 분리:
import styled from 'styled-components';
import * as Styled from './컴포넌트명.styles';
export const Container = styled.div`...`;
export const Label = styled.label`...`;
간단한 컴포넌트는 .tsx 파일 내 인라인으로 작성해도 무방.
주요 규칙
네이밍
- 컴포넌트 파일:
PascalCase.tsx
- 스타일 파일:
PascalCase.styles.ts
- 인터페이스:
컴포넌트명Props (예: ButtonProps, InputFieldProps)
- styled-components 내부 전달 prop:
$ 접두사 (예: $animated, $width)
타입 안전성
any 금지
- 제네릭 활용 (예:
CustomDropDown<T extends string | number>)
- 옵셔널 props에는 적절한 기본값 제공
접근성 (a11y)
- 네이티브 요소 래핑 시
extends HTMLXxxAttributes로 aria-* 자동 지원
- 역할이 명확한 경우
role 속성 명시 (예: role="listbox", role="option")
- 이미지에 반드시
alt 제공
- 키보드 인터랙션 고려 (포커스, Enter/Space 키)
styled-components
- 테마는
theme.colors, theme.typography, theme.transitions 활용
- 반응형은
src/styles/mediaQuery.ts의 브레이크포인트 사용
- 조건부 스타일은
css 헬퍼 함수로 타입 안전하게 작성
import styled, { css } from 'styled-components';
const StyledButton = styled.button<{ $animated: boolean }>`
${({ $animated }) =>
$animated &&
css`
animation: ${pulse} 0.4s ease-in-out;
`}
`;
체크리스트
새 공통 컴포넌트 생성 또는 수정 시 확인:
참고 파일
src/components/common/Button/Button.tsx - 기본 컴포넌트 패턴 (명시적 커스텀 props 인터페이스)
src/components/common/InputField/InputField.tsx - 복잡한 props 인터페이스 예시
src/components/common/Modal/PortalModal.tsx - 포털 컴포넌트 패턴
src/components/common/CustomDropDown/CustomDropDown.tsx - 복합 컴포넌트(Compound) 패턴
src/styles/mediaQuery.ts - 반응형 브레이크포인트
src/styles/theme/ - 테마 시스템 (colors, typography, transitions)
기술 스택
- React 19 + TypeScript
- styled-components (스타일링)
- Framer Motion (애니메이션이 필요한 경우)
- React Portal (모달, 오버레이)