React Input 컴포넌트 원페이지 가이드

Input · InputArea · InputPrice · InputCard · InputResident 코드 구조와 React 문법을 한 페이지에서 정리합니다.

1. 전체 구조부터 보기

Input └─ 일반 한 줄 입력 InputArea └─ 여러 줄 입력 → Input의 <input>이 <textarea>로 바뀐 형태 InputPrice └─ 금액 입력 → Input + 오른쪽 단위 "원" InputCard └─ 카드번호 입력 → input 4개 + "-" 구분자 InputResident └─ 주민번호 형태 입력 → input 2개 + "-" 구분자
Input가장 기본이 되는 한 줄 입력 컴포넌트
InputAreatextarea를 사용하는 여러 줄 입력
InputPrice입력값 오른쪽에 단위를 표시
InputCard4개의 input을 반복 생성
InputResident2개의 input을 반복 생성

2. Input.jsx

다섯 컴포넌트 중 가장 기본이 되는 형태입니다. label, 버튼, 입력창, 도움말/에러 메시지 구조를 가집니다.

<Input
  label="이름"
  placeholder="이름을 입력하세요"
  helperText="실명을 입력해주세요"
/>
이름
이름을 입력하세요
실명을 입력해주세요

message 결정

const message = errorText ?? helperText

??는 왼쪽 값이 null 또는 undefined일 때 오른쪽 값을 사용합니다.

errorText = "필수 입력입니다."
helperText = "이름을 입력하세요."

const message = errorText ?? helperText
// "필수 입력입니다."
errorText ↓ 없으면 helperText

invalid 기본값

invalid = Boolean(errorText)

에러 문구가 존재하면 자동으로 invalid 상태가 됩니다.

Boolean("에러입니다") // true
Boolean("")          // false
Boolean(undefined)   // false
Boolean(null)        // false
errorText 있음 ↓ invalid = true ↓ styles.invalid CSS 적용

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}
/>
Input에 전달 ↓ placeholder / maxLength / name / autoComplete / onChange ↓ ...props ↓ 실제 <input {...props} />로 전달

② 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="내용을 입력해주세요"
/>
Input을 이해했다면 InputArea는 사실상 input → textarea, 그리고 rows 추가 정도로 이해하면 됩니다.

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
카드번호
1234
-
5678
-
9012
-
3456

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개. 비제어 방식
inputPlaceholdersplaceholder 4개
inputNamesinput의 name 4개

input 4개 반복 생성

{Array.from(
  { length: CARD_PART_COUNT },
  (_, index) => (
    <div key={index}>
      <input ... />
    </div>
  )
)}
index = 0 → 첫 번째 input index = 1 → 두 번째 input index = 2 → 세 번째 input index = 3 → 네 번째 input

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>
)}
index 0 → 0 < 3 → "-" index 1 → 1 < 3 → "-" index 2 → 2 < 3 → "-" index 3 → 3 < 3 → false 최종 결과: input - input - input - input

7. InputResident.jsx

구조적으로는 InputCard와 거의 같습니다. 차이는 입력칸이 2개라는 점과 각 칸의 최대 입력 길이가 다르다는 점입니다.

const RESIDENT_PART_COUNT = 2
주민등록번호 형태
앞 6자리
-
뒤 7자리

maxLengths

maxLengths = [6, 7]
const inputMaxLengths =
  toParts(maxLengths, undefined)

결과는 다음과 같습니다.

[6, 7]

각 input에서:

maxLength={inputMaxLengths[index]}
index 0 → maxLength={6} index 1 → maxLength={7}
InputResident는 InputCard의 2칸 버전이라고 생각하면 가장 쉽습니다.

8. InputCard와 InputResident 비교

구분 InputCard InputResident
입력칸4개2개
개수 상수CARD_PART_COUNT = 4RESIDENT_PART_COUNT = 2
구분자--
기본 maxLength4[6, 7]
반복 생성Array.from()Array.from()
값 관리values / defaultValuesvalues / defaultValues
변경 이벤트(event, index)(event, index)

다섯 컴포넌트의 공통 레이아웃

┌─────────────────────────────┐ │ label button │ ← 공통 ├─────────────────────────────┤ │ │ │ 컴포넌트마다 다른 입력 영역 │ │ │ ├─────────────────────────────┤ │ helperText / errorText │ ← 공통 └─────────────────────────────┘

가운데 영역만 비교

Input [ input ] InputArea [ textarea ] [ ] InputPrice [ input 원 ] InputCard [ ] - [ ] - [ ] - [ ] InputResident [ ] - [ ]

className 관련 props

prop적용 위치
className컴포넌트 전체
fieldClassName입력 박스 영역
inputClassName실제 input
textareaClassName실제 textarea
labelClassNamelabel
buttonClassName상단 button
helperClassNamehelper/error 메시지
unitClassNameInputPrice의 단위
<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자리'
  ]}
/>

마지막 핵심 정리

Input한 줄 입력
InputArea여러 줄 입력
InputPrice한 줄 입력 + 단위
InputCard4개 분할 입력
InputResident2개 분할 입력
이 코드에서 특히 익혀둘 문법은 ??, ?., Boolean(), ...props, Array.from(), 반복 렌더링, value와 defaultValue, (event, index) 이벤트 전달입니다.