React Input 컴포넌트 원페이지 가이드
Input · InputArea · InputPrice · InputCard · InputResident 코드 구조와 React 문법을 한 페이지에서 정리합니다.
1. 전체 구조부터 보기
2. Input.jsx
다섯 컴포넌트 중 가장 기본이 되는 형태입니다. label, 버튼, 입력창, 도움말/에러 메시지 구조를 가집니다.
<Input
label="이름"
placeholder="이름을 입력하세요"
helperText="실명을 입력해주세요"
/>
message 결정
const message = errorText ?? helperText
??는 왼쪽 값이 null 또는 undefined일 때 오른쪽 값을 사용합니다.
errorText = "필수 입력입니다."
helperText = "이름을 입력하세요."
const message = errorText ?? helperText
// "필수 입력입니다."
invalid 기본값
invalid = Boolean(errorText)
에러 문구가 존재하면 자동으로 invalid 상태가 됩니다.
Boolean("에러입니다") // true
Boolean("") // false
Boolean(undefined) // false
Boolean(null) // false
hasButton
const hasButton = Boolean(buttonLabel)
buttonLabel이 있으면 버튼을 렌더링하기 위한 boolean 값입니다.
<Input
label="휴대폰 번호"
buttonLabel="인증"
onButtonClick={handleVerify}
/>
header 조건부 렌더링
{(label || hasButton) && (
<div className={styles.header}>
...
</div>
)}
label 또는 버튼 중 하나라도 있으면 header 영역을 만듭니다. 둘 다 없으면 header 자체를 렌더링하지 않습니다.
3. 공통으로 알아야 하는 React / JavaScript 문법
① ...props
export default function Input({
label,
type = 'text',
...props
})
컴포넌트에서 따로 꺼내지 않은 나머지 props를 한 객체로 모읍니다.
<Input
label="아이디"
placeholder="아이디 입력"
maxLength={20}
name="userId"
autoComplete="username"
onChange={handleChange}
/>
② cn()
className={cn(
styles.field,
disabled && styles.disabled,
invalid && styles.invalid,
className
)}
조건에 따라 CSS 클래스를 합치는 유틸 함수입니다. disabled가 true면 disabled 클래스가, invalid가 true면 invalid 클래스가 추가됩니다.
③ optional chaining ?.
onChange?.(event, index)
inputValues?.[index]
값이나 함수가 존재할 때만 접근 또는 실행합니다. 값이 없으면 에러 대신 undefined가 됩니다.
④ 조건부 렌더링 &&
{message && (
<span>{message}</span>
)}
message가 truthy일 때만 span을 렌더링합니다.
4. InputArea.jsx
Input과 구조는 거의 동일하고, 실제 입력 태그만 textarea로 바뀝니다.
<textarea
{...props}
className={cn(styles.textarea, textareaClassName)}
rows={rows}
disabled={disabled}
/>
기본값은 다음과 같습니다.
rows = 5
<InputArea
label="문의 내용"
rows={8}
placeholder="내용을 입력해주세요"
/>
5. InputPrice.jsx
기본 Input 구조에 오른쪽 단위 표시가 추가된 형태입니다.
unit = '원'
<div className={cn(styles.box, fieldClassName)}>
<input ... />
{unit && (
<span className={cn(styles.unit, unitClassName)}>
{unit}
</span>
)}
</div>
inputMode
inputMode={type === 'text' ? 'numeric' : undefined}
기본 type이 text인 경우 모바일 브라우저에 숫자 키패드를 보여달라는 힌트를 줍니다.
<input
type="text"
inputMode="numeric"
/>
inputMode="numeric"은 숫자만 입력되도록 강제하는 기능이 아닙니다. 숫자 이외 값을 막으려면 별도 검증 로직이 필요합니다.6. InputCard.jsx
카드번호처럼 4개 영역으로 나뉜 값을 입력하기 위한 컴포넌트입니다.
const CARD_PART_COUNT = 4
toParts() 함수
function toParts(value, fallback) {
if (Array.isArray(value)) {
return Array.from(
{ length: CARD_PART_COUNT },
(_, index) => value[index] ?? ''
)
}
return Array.from(
{ length: CARD_PART_COUNT },
() => fallback
)
}
핵심 목적은 어떤 값이 들어오더라도 4개짜리 배열 형태로 맞추는 것입니다.
toParts(['1111', '2222'], '')
// 결과
[
'1111',
'2222',
'',
''
]
배열 자체가 전달되지 않은 경우에는 fallback으로 4개를 채웁니다.
toParts(undefined, '문구')
// 결과
[
'문구',
'문구',
'문구',
'문구'
]
입력 데이터 준비
const inputValues =
Array.isArray(values) ? toParts(values, '') : null
const inputDefaultValues =
toParts(defaultValues, '')
const inputPlaceholders =
toParts(placeholders, '문구')
const inputNames =
toParts(names, undefined)
| 변수 | 역할 |
|---|---|
| inputValues | 현재 입력값 4개. 제어 컴포넌트 방식 |
| inputDefaultValues | 최초 초기값 4개. 비제어 방식 |
| inputPlaceholders | placeholder 4개 |
| inputNames | input의 name 4개 |
input 4개 반복 생성
{Array.from(
{ length: CARD_PART_COUNT },
(_, index) => (
<div key={index}>
<input ... />
</div>
)
)}
value와 defaultValue
defaultValue={
inputValues ? undefined : inputDefaultValues[index]
}
value={inputValues?.[index]}
| 방식 | 예제 | 의미 |
|---|---|---|
| 비제어 | <input defaultValue="1234" /> |
초기값만 React가 넣고 이후 상태는 DOM이 관리 |
| 제어 | <input value={value} onChange={...} /> |
현재 값을 React state 등이 관리 |
onChange에서 index 같이 전달
onChange={(event) => onChange?.(event, index)}
<InputCard
onChange={(event, index) => {
console.log(event.target.value)
console.log(index)
}}
/>
두 번째 입력창에 5678을 입력했다면:
event.target.value // "5678"
index // 1
마지막에는 '-'를 붙이지 않기
{index < CARD_PART_COUNT - 1 && (
<span className={styles.separator}>-</span>
)}
7. InputResident.jsx
구조적으로는 InputCard와 거의 같습니다. 차이는 입력칸이 2개라는 점과 각 칸의 최대 입력 길이가 다르다는 점입니다.
const RESIDENT_PART_COUNT = 2
maxLengths
maxLengths = [6, 7]
const inputMaxLengths =
toParts(maxLengths, undefined)
결과는 다음과 같습니다.
[6, 7]
각 input에서:
maxLength={inputMaxLengths[index]}
8. InputCard와 InputResident 비교
| 구분 | InputCard | InputResident |
|---|---|---|
| 입력칸 | 4개 | 2개 |
| 개수 상수 | CARD_PART_COUNT = 4 | RESIDENT_PART_COUNT = 2 |
| 구분자 | - | - |
| 기본 maxLength | 4 | [6, 7] |
| 반복 생성 | Array.from() | Array.from() |
| 값 관리 | values / defaultValues | values / defaultValues |
| 변경 이벤트 | (event, index) | (event, index) |
다섯 컴포넌트의 공통 레이아웃
가운데 영역만 비교
className 관련 props
| prop | 적용 위치 |
|---|---|
| className | 컴포넌트 전체 |
| fieldClassName | 입력 박스 영역 |
| inputClassName | 실제 input |
| textareaClassName | 실제 textarea |
| labelClassName | label |
| buttonClassName | 상단 button |
| helperClassName | helper/error 메시지 |
| unitClassName | InputPrice의 단위 |
<Input
label="이름"
className={styles.myField}
inputClassName={styles.myInput}
/>
cn(styles.input, inputClassName)
이 방식은 컴포넌트의 기본 CSS는 유지하면서 사용하는 화면에서 추가 CSS를 적용할 수 있게 해줍니다.
9. 실제 사용 예제
<Input
label="이름"
placeholder="이름 입력"
/>
<InputArea
label="문의사항"
placeholder="내용 입력"
rows={5}
/>
<InputPrice
label="보험료"
placeholder="금액 입력"
unit="원"
/>
<InputCard
label="카드번호"
placeholders={[
'1234',
'5678',
'9012',
'3456'
]}
/>
<InputResident
label="주민등록번호"
placeholders={[
'앞 6자리',
'뒤 7자리'
]}
/>