Skip to main content

커스텀 컴포넌트 작성하기

커스텀 컴포넌트는 일반적인 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로 감싸서 내보내는 것이 좋습니다.