소프트웨어 디자인, 설계
공통 컴포넌트 설계
공통 컴포넌트 설계는 상당히 중요합니다. 특히 어드민이나 물슈시스템 혹은 대부분의 페이지에서 공통으로 쓰는 필터나 테이블 같은 경우 모든 페이지가 획일화된 패턴으로 되어 있습니다. 그렇다는 것은 필터나, dataGrid, 팝업이나 자주쓰이는 ui 공통 컴포넌트설계를 잘못하면 유지보수 할 때도 잘못된 설계때문에 10배, 재수없으면 100배 이상의 공수가 더 투입되는 경우도 있습니다
최악의 상황은 설계를 뜯어고쳐야 하는 경우까지도 생깁니다. 근데 뜯어고쳐도 잘 된다리란 보장도 애매하구요, 무중단 배포했을 때 리스크도 있구요, 회원에게 안겨지는 피해 까지 생각하면 손해가 어마어마합니다. 그래서 공통컴포넌트 설계가 우선순위가 높은 이유입니다.
그래서 관리자 입장에서는 뭐 별거아닌거 같아 보이는데 일주일 하루웬종일 개발자가 수정에 시간을 투자하고 있으니 이해가 안되고, 개발자도 이거 뭐 인계를 받아서 하긴 하는데 어디서부터 손대야할지 감이 안잡히고 오래걸린다고 하면 실력없는 개발자로 비칠까봐 말은 못하겠고 서로 타협이 안되서 마찰이 생겨 개발자가 도주하는 경우도 있습니다.
이에 대한 해결방법은 이러한 부분은 개발자가 허심탄회하게 고객사 또는 관리자에게 공유하는 것입니다. 뭐 못한다고 욕좀 먹으면 어떻습니까 문제를 해결하는게 중요한겁니다.
결국 소통을 잘해야 합니다. 충분히 이해를 시키고 설계의 중요성을 공유하고 일정을 수용할 수 있을정도로 가져갑니다. 해당부분은 개발일정이 오래걸리는 감이 있더라도 테스트도 꼼꼼히 진행해야 합니다.
1. 재사용 가능성
컴포넌트는 특정 상황에 국한되지 않고 다양한 곳에서 사용할 수 있도록 설계되어야 합니다.
Props를 사용해 동적으로 설정 가능하게 하고, 하드코딩된 값을 피합니다.예: 버튼 컴포넌트는
label
,onClick
,type
과 같은 프로퍼티를 받아 다양한 용도로 사용할 수 있도록 설계합니다.설명: 이 버튼은
variant
(스타일)과size
(크기)를 props로 받아 다양한 디자인 요구사항을 충족할 수 있습니다.
const Button = ({ label, onClick, variant = 'primary', size = 'medium' }) => {
const classNames = `btn ${variant} ${size}`
return (
<button className={classNames} onClick={onClick}>
{label}
</button>
)
}
2. 단일 책임 원칙
- 컴포넌트는 하나의 명확한 역할만 수행해야 합니다.
- 복잡한 작업은 하위 컴포넌트로 분리하고, 공통 컴포넌트는 핵심 역할만 처리하도록 제한합니다.
- 설명: 입력 필드와 레이블을 하나의 책임으로 묶되, 컴포넌트는 입력 필드와 관련된 역할만 수행합니다.
const InputField = ({ id, label, type = 'text', value, onChange }) => {
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type={type} value={value} onChange={onChange} />
</div>
)
}
3. 확장 가능성
- Slot이나 Children을 활용하여 컴포넌트 내부를 쉽게 커스터마이징할 수 있도록 만듭니다.
- 예: 카드 컴포넌트는 헤더와 본문, 푸터를
children
으로 전달받아 다양한 구조로 사용할 수 있도록 합니다. - 설명: 카드의 내용은 children으로, 헤더와 푸터는 선택적으로 전달하여 확장성을 높입니다.
const Card = ({ children, header, footer }) => {
return (
<div className="card">
{header && <div className="card-header">{header}</div>}
<div className="card-body">{children}</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
)
}
4. 일관성
- 근데 모든 컴포넌트를 하나의 컴포넌트로 통일시켜야 한다는 법칙은 없습니다. 버튼 같은 경우도 인증 관련 버튼, 필터 관련 버튼 이런 식으로 공통버튼을 다르게 설계하는 경우도 있습니다. 그것은 회사 내부적으로 협의하시기 바랍니다.
5. 성능 최적화
- 불필요한 렌더링을 방지하기 위해 React의 경우 React.memo나 useMemo를 활용합니다.
- Lazy Loading을 고려하여 컴포넌트를 필요한 시점에 로드합니다.
- 설명: React.memo를 사용하여 props가 변경되지 않으면 불필요한 재렌더링을 방지합니다.
const MemoizedComponent = React.memo(({ data }) => {
console.log('Rendered')
return <div>{data}</div>
})
6. 접근성
- 불필요한 렌더링을 방지하기 위해 React의 경우 React.memo나 useMemo를 활용합니다.
- Lazy Loading을 고려하여 컴포넌트를 필요한 시점에 로드합니다.
- 설명: aria-label 속성을 추가해 스크린 리더 사용자가 버튼의 목적을 이해할 수 있도록 합니다.
const AccessibleButton = ({ label, onClick }) => {
return (
<button aria-label={label} onClick={onClick}>
{label}
</button>
)
}
7. 테마와 스타일링
- CSS-in-JS 또는 CSS Modules과 같은 기술을 활용하여 스타일이 충돌하지 않도록 합니다.
- 테마 지원을 고려하여 다크 모드, 사용자 지정 색상을 쉽게 적용할 수 있도록 설계합니다.
- 설명: 테마에 따라 버튼 스타일을 쉽게 변경할 수 있습니다.
const ThemeButton = ({ label, theme }) => {
return <button className={`btn-${theme}`}>{label}</button>
}
8. 테스트 가능성
- 공통 컴포넌트는 유닛 테스트를 통해 기능을 보장할 수 있어야 합니다.
- 컴포넌트가 의존하는 외부 요소를 최소화하여 독립적으로 테스트 가능하게 설계합니다.
- 설명: 테스트 가능한 컴포넌트를 설계하고, Jest와 같은 테스트 도구로 검증합니다.
// Jest 테스트
test('Button renders with correct label', () => {
const { getByText } = render(<Button label="Click Me" />)
expect(getByText('Click Me')).toBeInTheDocument()
})
9. 문서화
- 컴포넌트의 사용 방법과 props의 의미를 명확히 문서화합니다.
- 예: Storybook이나 JSDoc을 사용해 UI 및 코드 문서를 작성합니다.
10. 버전 관리 및 하위 호환성
- 기존에 사용하던 컴포넌트를 대체하거나 변경할 때 하위 호환성을 유지하도록 설계합니다.
- 큰 변경이 필요하다면 버전을 분리하거나 마이그레이션 가이드를 제공해야 합니다.
const DeprecatedButton = (props) => {
console.warn('This component is deprecated. Use NewButton instead.')
return <Button {...props} />
}
11. 코드 간소화
- 필요 이상으로 복잡하지 않은, 이해하기 쉬운 컴포넌트를 만듭니다.
- 한눈에 구조와 역할을 이해할 수 있도록 명확하게 작성합니다.
const SimpleComponent = ({ isActive }) => {
const className = isActive ? 'active' : 'inactive'
return <div className={className}>Hello</div>
}
12. 안정성 및 에러처리
- 컴포넌트가 비정상적인 입력을 받았을 때도 안정적으로 작동하도록 설계합니다.
- 기본값 설정과 에러 경고를 추가합니다.
- 설명: 기본값을 설정하고 예상치 못한 오류를 대비합니다.
const SafeComponent = ({ text = 'Default Text' }) => {
if (!text) {
console.error('Text prop is required')
return null
}
return <div>{text}</div>
}