Version: 2.0.0 Last Updated: 2026-02-05
Input 컴포넌트는 폼 입력 필드를 위한 기본 컴포넌트와 검색 전용 컴포넌트를 제공합니다. 아이콘 슬롯, 라벨, 헬퍼 텍스트, 에러 상태를 지원하며, CVA를 사용한 variant 시스템을 제공합니다.
파일 위치: packages/web/lib/design-system/input.tsx
시각적 참고: decoded.pen
// 컴포넌트 import
import { Input, SearchInput } from "@/lib/design-system";
// Variants import (커스텀 스타일링)
import { inputVariants } from "@/lib/design-system";
// TypeScript types
import type { InputProps, SearchInputProps } from "@/lib/design-system";기본 입력 필드 컴포넌트입니다. 아이콘 슬롯 (leftIcon, rightIcon), 라벨, 헬퍼 텍스트, 에러 상태를 지원합니다.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
"default" | "error" | "search" |
"default" |
입력 필드 스타일 변형 |
leftIcon |
ReactNode |
- | 왼쪽 아이콘 (자동으로 pl-10 패딩 적용) |
rightIcon |
ReactNode |
- | 오른쪽 아이콘 (자동으로 pr-10 패딩 적용) |
label |
string |
- | 라벨 텍스트 (위에 표시) |
helperText |
string |
- | 도움말 텍스트 (아래에 표시) |
error |
string |
- | 에러 메시지 (variant="error" 자동 적용) |
type |
string |
"text" |
Input 타입 (text, email, password 등) |
className |
string |
- | 추가 CSS 클래스 |
| ...rest | InputHTMLAttributes<HTMLInputElement> |
- | 표준 input 속성 |
| Variant | Style | Usage |
|---|---|---|
default |
기본 테두리, focus ring | 일반 입력 필드 |
error |
빨간 테두리, destructive focus ring | 에러 상태 (error prop 있으면 자동 적용) |
search |
둥근 모서리 (rounded-full), pl-10 pr-10 | 검색 입력 (SearchInput 전용) |
import { Input } from "@/lib/design-system";
function BasicForm() {
return (
<>
{/* 기본 입력 */}
<Input placeholder="Enter text..." />
{/* 라벨이 있는 입력 */}
<Input
label="Email"
placeholder="you@example.com"
type="email"
/>
{/* 헬퍼 텍스트 */}
<Input
label="Password"
helperText="Must be at least 8 characters"
type="password"
/>
</>
);
}import { Input } from "@/lib/design-system";
import { Mail, Lock, Eye } from "lucide-react";
function IconInputs() {
return (
<>
{/* 왼쪽 아이콘 */}
<Input
leftIcon={<Mail className="h-4 w-4" />}
placeholder="Email"
/>
{/* 오른쪽 아이콘 */}
<Input
rightIcon={<Eye className="h-4 w-4" />}
placeholder="Password"
type="password"
/>
{/* 양쪽 아이콘 */}
<Input
leftIcon={<Lock className="h-4 w-4" />}
rightIcon={<Eye className="h-4 w-4" />}
placeholder="Enter secure code"
/>
</>
);
}import { Input } from "@/lib/design-system";
function FormWithValidation() {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
return (
<Input
label="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
error={error} // error prop 있으면 variant="error" 자동 적용
placeholder="you@example.com"
/>
);
}에러 동작:
errorprop이 있으면 자동으로variant="error"적용helperText는 숨겨지고error메시지만 표시- 빨간 테두리와 destructive focus ring 스타일 적용
import { Input, Text } from "@/lib/design-system";
import { Mail, Lock } from "lucide-react";
function LoginForm() {
return (
<form className="space-y-6">
<div>
<Input
label="Email Address"
type="email"
leftIcon={<Mail className="h-4 w-4" />}
placeholder="you@example.com"
required
/>
</div>
<div>
<Input
label="Password"
type="password"
leftIcon={<Lock className="h-4 w-4" />}
placeholder="Enter password"
helperText="Minimum 8 characters"
required
/>
</div>
<button type="submit" className="btn-primary">
Login
</button>
</form>
);
}직접 variant 클래스를 사용하려면:
import { inputVariants } from "@/lib/design-system";
const inputClasses = inputVariants({ variant: "default" });
// Returns: "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ..."검색 전용 입력 컴포넌트입니다.
내장 Search 아이콘과 Clear 버튼을 자동으로 표시하며, variant="search" (둥근 모서리)가 기본 적용됩니다.
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string |
- | 입력 값 (제어 컴포넌트) |
onChange |
(e: ChangeEvent) => void |
- | 입력 변경 핸들러 |
onClear |
() => void |
- | Clear 버튼 클릭 핸들러 |
placeholder |
string |
"Search..." |
Placeholder 텍스트 |
autoFocus |
boolean |
- | 자동 포커스 |
className |
string |
- | 추가 CSS 클래스 |
| ...rest | Omit<InputProps, "leftIcon" | "rightIcon" | "variant"> |
- | Input props (아이콘, variant 제외) |
- 자동 Search 아이콘: 왼쪽에 항상 표시 (Lucide
Search아이콘) - 조건부 Clear 버튼:
value가 있을 때만 오른쪽에 표시 (LucideX아이콘) - 둥근 모서리:
variant="search"(rounded-full) 자동 적용 - Interactive Clear: Clear 버튼에 hover 상태와 transition 적용
import { SearchInput } from "@/lib/design-system";
function SearchBar() {
const [query, setQuery] = useState('');
return (
<SearchInput
value={query}
onChange={(e) => setQuery(e.target.value)}
onClear={() => setQuery('')}
placeholder="Search products..."
/>
);
}import { SearchInput } from "@/lib/design-system";
function SearchOverlay() {
const [query, setQuery] = useState('');
return (
<div className="fixed inset-0 z-50 bg-background p-4">
<SearchInput
value={query}
onChange={(e) => setQuery(e.target.value)}
onClear={() => setQuery('')}
placeholder="Search images, posts, users..."
autoFocus // 오버레이 열릴 때 자동 포커스
/>
{query && (
<div className="mt-4">
<p>Search results for: {query}</p>
</div>
)}
</div>
);
}import { SearchInput } from "@/lib/design-system";
function Header() {
const [query, setQuery] = useState('');
return (
<header className="flex items-center gap-4 p-4">
<h1>Logo</h1>
{/* 헤더 내 검색 입력 */}
<SearchInput
value={query}
onChange={(e) => setQuery(e.target.value)}
onClear={() => setQuery('')}
className="max-w-sm"
/>
<button>User Menu</button>
</header>
);
}import { SearchInput } from "@/lib/design-system";
import { useDebounce } from "@/lib/hooks";
function DebouncedSearch() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
// debouncedQuery가 변경되면 검색 API 호출
useEffect(() => {
if (debouncedQuery) {
fetchSearchResults(debouncedQuery);
}
}, [debouncedQuery]);
return (
<SearchInput
value={query}
onChange={(e) => setQuery(e.target.value)}
onClear={() => setQuery('')}
placeholder="Type to search..."
/>
);
}import { Input, Text } from "@/lib/design-system";
function FormField({ label, error, ...inputProps }) {
return (
<div className="space-y-1.5">
<Input
label={label}
error={error}
{...inputProps}
/>
</div>
);
}import { Input } from "@/lib/design-system";
function ValidatedInput({ value, onChange }) {
const [touched, setTouched] = useState(false);
const error = touched && !value ? "This field is required" : "";
return (
<Input
value={value}
onChange={onChange}
onBlur={() => setTouched(true)}
error={error}
label="Required Field"
/>
);
}import { Input } from "@/lib/design-system";
import { Eye, EyeOff } from "lucide-react";
function PasswordInput() {
const [showPassword, setShowPassword] = useState(false);
return (
<Input
type={showPassword ? "text" : "password"}
label="Password"
rightIcon={
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="pointer-events-auto cursor-pointer"
>
{showPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
}
/>
);
}Input 컴포넌트는 자동으로 label과 input을 연결합니다:
// ✅ 자동 연결
<Input label="Email" />
// ✅ 수동 연결
<label htmlFor="email">Email</label>
<input id="email" />에러 메시지는 aria-invalid와 함께 표시됩니다:
<Input error="Invalid email" />
// Renders: <input aria-invalid="true" /><Input label="Email" required />
// label에 자동으로 asterisk (*) 추가 가능Input 컴포넌트는 다음 토큰을 사용합니다:
colors.border: 기본 테두리colors.input: 입력 필드 테두리colors.ring: Focus ring 색상colors.destructive: 에러 상태 테두리colors.background: 입력 배경colors.foreground: 텍스트 색상colors.mutedForeground: Placeholder 색상borderRadius.md: 기본 모서리 (Input)borderRadius.full: 둥근 모서리 (SearchInput)
자세한 내용: tokens.md
- Design Tokens - Color, Spacing 토큰
- Typography Components - Text 컴포넌트
- Design Patterns - 폼 패턴 가이드
- decoded.pen - 시각적 레퍼런스
Note: Input과 SearchInput은 제어 컴포넌트로 사용하는 것을 권장합니다.
value와onChangeprops를 항상 함께 전달하세요.