커스텀 컴포넌트 작성하기
커스텀 컴포넌트는 일반적인 React 컴포넌트와 동일한 방식으로 작성됩니다. 이 문서에서는 커스텀 컴포넌트 작성에 필요한 기본 개념과 예시 코드를 소개합니다.
기본 구조
커스텀 컴포넌트의 기본 구조는 다음과 같습니다:
import { memo, ReactElement } from 'react';
import { CustomComponentRenderProps, useComponentState, useLogger } from '@hops/custom-component';
import { FormControl, FormLabel, Input } from '@hops/design-system';
interface OwnProps {
label: string;
defaultValue: string;
}
function MyComponent(props: CustomComponentRenderProps<OwnProps>): ReactElement {
const { label, defaultValue } = props;
const logger = useLogger();
const [value, setValue] = useComponentState<string>('inputValue', defaultValue);
return (
<FormControl>
<FormLabel>{label}</FormLabel>
<Input
value={value}
onChange={(e) => {
setValue(e.target.value);
logger.debug(`onChange called: ${e.target.value}`);
}}
/>
</FormControl>
);
}
export default memo(MyComponent);
컴포넌트 작성 단계
1. Props 정의하기
커스텀 컴포넌트는 OwnProps
인터페이스를 통해 외부에서 받을 Props를 정의합니다:
interface OwnProps {
// 사용자가 에디터에서 설정할 Props
label: string;
placeholder: string;
required: boolean;
}
이 Props들은 에디터에서 사용자가 설정할 수 있으며, 컴포넌트 내부에서 사용됩니다.
2. 상태 관리하기
컴포넌트의 상태는 useComponentState
훅을 사용하여 관리합니다.
const [value, setValue] = useComponentState<string>('fieldValue', defaultValue);
커스텀 컴포넌트 구현 시 useComponentState
로 정의한 상태는 외부에서도 접근할 수 있습니다.
필요한 곳에서 컴포넌트_이름.상태_이름
형식의 접근자로 커스텀 컴포넌트의 상태에 접근합니다.
customComponent1.fieldValue;
이렇게 관리되는 상태는 다른 컴포넌트나 워크플로우에서 접근하고 사용할 수 있습니다. 물론 React의 useState를 사용하여 내부에서만 사용할 상태도 관리할 수 있습니다.
3. UI 구현하기
일반적인 React 컴포넌트처럼 JSX를 사용하여 UI를 구현합니다:
return (
<FormControl required={required}>
<FormLabel>{label}</FormLabel>
<Input value={value} placeholder={placeholder} onChange={(e) => setValue(e.target.value)} />
</FormControl>
);
Hops의 디자인 시스템 컴포넌트(@hops/design-system
)를 사용하면 일관된 디자인을 유지할 수 있습니다.
예시: 커스텀 입력 필드
다음은 사용자 정의 유효성 검사 기능이 있는 텍스트 필드 예시입니다:
import { memo, ReactElement, useState } from 'react';
import { CustomComponentRenderProps, useComponentState } from '@hops/custom-component';
import { FormControl, FormLabel, Input, FormHelperText } from '@hops/design-system';
// 컴포넌트 Props 타입 정의
interface OwnProps {
label: string;
placeholder: string;
pattern: string; // 정규식 패턴
errorMessage: string;
}
// 컴포넌트 구현
function PatternValidatedInput(props: CustomComponentRenderProps<OwnProps>): ReactElement {
// 컴포넌트 프로퍼티 읽기
const { label, placeholder, pattern, errorMessage } = props;
// 상태 관리
const [value, setValue] = useComponentState<string>('value', '');
const [isValid, setIsValid] = useComponentState<boolean>('isValid', true);
const validateInput = (input: string) => {
if (!pattern) return true;
const regex = new RegExp(pattern);
return regex.test(input);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setValue(newValue);
const valid = validateInput(newValue);
setIsValid(valid);
};
return (
<FormControl isInvalid={!isValid}>
<FormLabel>{label}</FormLabel>
<Input value={value} placeholder={placeholder} onChange={handleChange} />
{!isValid && <FormHelperText color="red">{errorMessage}</FormHelperText>}
</FormControl>
);
}
export default memo(PatternValidatedInput);
예시: 별점 컴포넌트
다음은 사용자 정의 별점 입력 컴포넌트의 예시입니다:
import { memo, ReactElement, useCallback, useState } from 'react';
import { CustomComponentRenderProps, useComponentState } from '@hops/custom-component';
// 컴포넌트 Props 타입 정의
interface OwnProps {
label: string;
maxStars: number;
size: 'sm' | 'md' | 'lg';
color: string;
}
// 컴포넌트 구현
function StarRating(props: CustomComponentRenderProps<OwnProps>): ReactElement {
// 컴포넌트 프로퍼티 읽기
const { label, maxStars = 5, size = 'md', color = 'gold' } = props;
// 상태 관리
const [rating, setRating] = useComponentState<number>('rating', 0);
const [hoveredRating, setHoveredRating] = useState(0);
const handleClick = useCallback(
(index: number) => {
setRating(index);
},
[setRating],
);
const starSizes = {
sm: { fontSize: '1rem', spacing: 8 },
md: { fontSize: '1.5rem', spacing: 16 },
lg: { fontSize: '2rem', spacing: 24 },
};
return (
<div>
<label>{label}</label>
<div style={{ display: 'flex', gap: starSizes[size].spacing }}>
{Array.from({ length: maxStars }).map((_, i) => (
<span
key={i}
style={{
cursor: 'pointer',
fontSize: starSizes[size].fontSize,
color: color,
}}
onMouseEnter={() => setHoveredRating(i + 1)}
onMouseLeave={() => setHoveredRating(0)}
onClick={() => handleClick(i + 1)}
>
{(hoveredRating || rating) > i ? '★' : '☆'}
</span>
))}
</div>
<div>
{rating} / {maxStars}
</div>
</div>
);
}
export default memo(StarRating);
tip
커스텀 컴포넌트를 작성할 때는 항상 메모이제이션을 적용하여 성능을 최적화하세요. 모든 커스텀 컴포넌트는 memo
로 감싸서 내보내는 것이 좋습니다.