diff --git a/package.json b/package.json
index 83740d67..fbeb6029 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
]
},
"dependencies": {
+ "@tanstack/react-form": "^1.27.0",
"@tanstack/react-query": "^5.90.3",
"@tanstack/react-query-devtools": "^5.90.2",
"axios": "^1.13.2",
@@ -50,7 +51,8 @@
"react": "19.2.0",
"react-dom": "19.2.0",
"swiper": "^12.0.3",
- "tailwind-merge": "^3.3.1"
+ "tailwind-merge": "^3.3.1",
+ "zod": "^4.1.13"
},
"devDependencies": {
"@commitlint/cli": "^20.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 7225477d..1df65272 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
+ '@tanstack/react-form':
+ specifier: ^1.27.0
+ version: 1.27.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
'@tanstack/react-query':
specifier: ^5.90.3
version: 5.90.3(react@19.2.0)
@@ -41,6 +44,9 @@ importers:
tailwind-merge:
specifier: ^3.3.1
version: 3.3.1
+ zod:
+ specifier: ^4.1.13
+ version: 4.1.13
devDependencies:
'@commitlint/cli':
specifier: ^20.1.0
@@ -2103,12 +2109,32 @@ packages:
'@tailwindcss/postcss@4.1.14':
resolution: {integrity: sha512-BdMjIxy7HUNThK87C7BC8I1rE8BVUsfNQSI5siQ4JK3iIa3w0XyVvVL9SXLWO//CtYTcp1v7zci0fYwJOjB+Zg==}
+ '@tanstack/devtools-event-client@0.3.5':
+ resolution: {integrity: sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw==}
+ engines: {node: '>=18'}
+
+ '@tanstack/form-core@1.27.0':
+ resolution: {integrity: sha512-QFEhg9/VcrwtpbcN7Qpl8JVVfEm2UJ+dzfDFGGMYub2J9jsgrp2HmaY7LSLlnkpTJlCIDxQiWDkiOFYQtK6yzw==}
+
+ '@tanstack/pacer@0.15.4':
+ resolution: {integrity: sha512-vGY+CWsFZeac3dELgB6UZ4c7OacwsLb8hvL2gLS6hTgy8Fl0Bm/aLokHaeDIP+q9F9HUZTnp360z9uv78eg8pg==}
+ engines: {node: '>=18'}
+
'@tanstack/query-core@5.90.3':
resolution: {integrity: sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==}
'@tanstack/query-devtools@5.90.1':
resolution: {integrity: sha512-GtINOPjPUH0OegJExZ70UahT9ykmAhmtNVcmtdnOZbxLwT7R5OmRztR5Ahe3/Cu7LArEmR6/588tAycuaWb1xQ==}
+ '@tanstack/react-form@1.27.0':
+ resolution: {integrity: sha512-7MBOtvjlUwkGpvA9TIOs3YdLoyfJWZYtxuAQIdkLDZ9HLrRaRbxWQIZ2H6sRVA35sPvx6uiQMunGHOPKip5AZA==}
+ peerDependencies:
+ '@tanstack/react-start': '*'
+ react: ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@tanstack/react-start':
+ optional: true
+
'@tanstack/react-query-devtools@5.90.2':
resolution: {integrity: sha512-vAXJzZuBXtCQtrY3F/yUNJCV4obT/A/n81kb3+YqLbro5Z2+phdAbceO+deU3ywPw8B42oyJlp4FhO0SoivDFQ==}
peerDependencies:
@@ -2120,6 +2146,18 @@ packages:
peerDependencies:
react: ^18 || ^19
+ '@tanstack/react-store@0.8.0':
+ resolution: {integrity: sha512-1vG9beLIuB7q69skxK9r5xiLN3ztzIPfSQSs0GfeqWGO2tGIyInZx0x1COhpx97RKaONSoAb8C3dxacWksm1ow==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+ '@tanstack/store@0.7.7':
+ resolution: {integrity: sha512-xa6pTan1bcaqYDS9BDpSiS63qa6EoDkPN9RsRaxHuDdVDNntzq3xNwR5YKTU/V3SkSyC9T4YVOPh2zRQN0nhIQ==}
+
+ '@tanstack/store@0.8.0':
+ resolution: {integrity: sha512-Om+BO0YfMZe//X2z0uLF2j+75nQga6TpTJgLJQBiq85aOyZNIhkCgleNcud2KQg4k4v9Y9l+Uhru3qWMPGTOzQ==}
+
'@testing-library/dom@10.4.1':
resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==}
engines: {node: '>=18'}
@@ -6276,6 +6314,11 @@ packages:
resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==}
engines: {node: '>= 0.4'}
+ use-sync-external-store@1.6.0:
+ resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -6498,8 +6541,8 @@ packages:
peerDependencies:
zod: ^3.25.0 || ^4.0.0
- zod@4.1.12:
- resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==}
+ zod@4.1.13:
+ resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
snapshots:
@@ -8872,10 +8915,31 @@ snapshots:
postcss: 8.5.6
tailwindcss: 4.1.14
+ '@tanstack/devtools-event-client@0.3.5': {}
+
+ '@tanstack/form-core@1.27.0':
+ dependencies:
+ '@tanstack/devtools-event-client': 0.3.5
+ '@tanstack/pacer': 0.15.4
+ '@tanstack/store': 0.7.7
+
+ '@tanstack/pacer@0.15.4':
+ dependencies:
+ '@tanstack/devtools-event-client': 0.3.5
+ '@tanstack/store': 0.7.7
+
'@tanstack/query-core@5.90.3': {}
'@tanstack/query-devtools@5.90.1': {}
+ '@tanstack/react-form@1.27.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@tanstack/form-core': 1.27.0
+ '@tanstack/react-store': 0.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+ react: 19.2.0
+ transitivePeerDependencies:
+ - react-dom
+
'@tanstack/react-query-devtools@5.90.2(@tanstack/react-query@5.90.3(react@19.2.0))(react@19.2.0)':
dependencies:
'@tanstack/query-devtools': 5.90.1
@@ -8887,6 +8951,17 @@ snapshots:
'@tanstack/query-core': 5.90.3
react: 19.2.0
+ '@tanstack/react-store@0.8.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+ dependencies:
+ '@tanstack/store': 0.8.0
+ react: 19.2.0
+ react-dom: 19.2.0(react@19.2.0)
+ use-sync-external-store: 1.6.0(react@19.2.0)
+
+ '@tanstack/store@0.7.7': {}
+
+ '@tanstack/store@0.8.0': {}
+
'@testing-library/dom@10.4.1':
dependencies:
'@babel/code-frame': 7.27.1
@@ -10573,8 +10648,8 @@ snapshots:
'@babel/parser': 7.28.4
eslint: 9.37.0(jiti@2.6.1)
hermes-parser: 0.25.1
- zod: 4.1.12
- zod-validation-error: 4.0.2(zod@4.1.12)
+ zod: 4.1.13
+ zod-validation-error: 4.0.2(zod@4.1.13)
transitivePeerDependencies:
- supports-color
@@ -13634,6 +13709,10 @@ snapshots:
punycode: 1.4.1
qs: 6.14.0
+ use-sync-external-store@1.6.0(react@19.2.0):
+ dependencies:
+ react: 19.2.0
+
util-deprecate@1.0.2: {}
util@0.12.5:
@@ -13890,8 +13969,8 @@ snapshots:
yoctocolors-cjs@2.1.3: {}
- zod-validation-error@4.0.2(zod@4.1.12):
+ zod-validation-error@4.0.2(zod@4.1.13):
dependencies:
- zod: 4.1.12
+ zod: 4.1.13
- zod@4.1.12: {}
+ zod@4.1.13: {}
diff --git a/src/components/pages/login/index.ts b/src/components/pages/login/index.ts
new file mode 100644
index 00000000..61b7acac
--- /dev/null
+++ b/src/components/pages/login/index.ts
@@ -0,0 +1 @@
+export { LoginForm } from './login-form';
diff --git a/src/components/pages/login/login-form/index.tsx b/src/components/pages/login/login-form/index.tsx
new file mode 100644
index 00000000..774326fa
--- /dev/null
+++ b/src/components/pages/login/login-form/index.tsx
@@ -0,0 +1,111 @@
+'use client';
+
+import { useForm } from '@tanstack/react-form';
+
+import { FormInput } from '@/components/shared';
+import { Button } from '@/components/ui';
+import { loginSchema } from '@/lib/schema/auth';
+
+const getHintMessage = (errors: unknown[], isTouched: boolean, submissionAttempts: number) => {
+ const firstError = errors[0] as { message?: string } | undefined;
+ const showError = isTouched || submissionAttempts > 0;
+
+ return showError ? firstError?.message : undefined;
+};
+
+export const LoginForm = () => {
+ const form = useForm({
+ defaultValues: {
+ email: '',
+ password: '',
+ },
+ validators: {
+ onSubmit: loginSchema,
+ onChange: loginSchema,
+ },
+ onSubmit: async ({ value }) => {
+ // API 호출
+ alert('login:' + value.email);
+ },
+ });
+
+ return (
+
({
+ canSubmit: state.canSubmit,
+ isSubmitting: state.isSubmitting,
+ })}
+ >
+ {({ canSubmit, isSubmitting }) => {
+ const disabled = !canSubmit || isSubmitting;
+
+ return (
+
+ );
+ }}
+
+
+ );
+};
diff --git a/src/components/pages/signup/index.ts b/src/components/pages/signup/index.ts
new file mode 100644
index 00000000..93e783fa
--- /dev/null
+++ b/src/components/pages/signup/index.ts
@@ -0,0 +1 @@
+export { SignupForm } from './signup-form';
diff --git a/src/components/pages/signup/signup-form/index.tsx b/src/components/pages/signup/signup-form/index.tsx
new file mode 100644
index 00000000..3ca1b20c
--- /dev/null
+++ b/src/components/pages/signup/signup-form/index.tsx
@@ -0,0 +1,155 @@
+'use client';
+
+import { useForm } from '@tanstack/react-form';
+
+import { FormInput } from '@/components/shared';
+import { Button } from '@/components/ui';
+import { signupSchema } from '@/lib/schema/auth';
+
+const getHintMessage = (errors: unknown[], isTouched: boolean, submissionAttempts: number) => {
+ const firstError = errors[0] as { message?: string } | undefined;
+ const showError = isTouched || submissionAttempts > 0;
+
+ return showError ? firstError?.message : undefined;
+};
+
+export const SignupForm = () => {
+ const form = useForm({
+ defaultValues: {
+ email: '',
+ nickname: '',
+ password: '',
+ confirmPassword: '',
+ },
+ validators: {
+ onChange: signupSchema,
+ onSubmit: signupSchema,
+ },
+ onSubmit: async ({ value }) => {
+ // api 호출
+ alert('signup:' + value.nickname);
+ },
+ });
+
+ return (
+ ({
+ canSubmit: state.canSubmit,
+ isSubmitting: state.isSubmitting,
+ })}
+ >
+ {({ canSubmit, isSubmitting }) => {
+ const disabled = !canSubmit || isSubmitting;
+
+ return (
+
+ );
+ }}
+
+
+ );
+};
diff --git a/src/components/shared/form-input/index.stories.tsx b/src/components/shared/form-input/index.stories.tsx
index 06164adc..902197c1 100644
--- a/src/components/shared/form-input/index.stories.tsx
+++ b/src/components/shared/form-input/index.stories.tsx
@@ -11,7 +11,11 @@ const meta = {
},
args: {
labelName: '라벨',
- placeholder: '값을 입력하세요',
+ required: true,
+ inputProps: {
+ type: 'text',
+ placeholder: '값을 입력하세요',
+ },
},
} satisfies Meta;
@@ -22,42 +26,48 @@ type Story = StoryObj;
// 기본 텍스트 인풋
export const Default: Story = {
args: {
- type: 'text',
hintMessage: '',
- required: false,
+ required: true,
+ inputProps: {
+ type: 'text',
+ placeholder: '값을 입력하세요',
+ },
},
};
// 힌트가 있는 인풋
export const WithHint: Story = {
args: {
- type: 'text',
labelName: '이메일',
- placeholder: 'name@example.com',
- hintMessage: '가입에 사용할 이메일을 입력하세요.',
- required: true,
+ required: false,
+ inputProps: {
+ type: 'text',
+ placeholder: 'name@example.com',
+ },
},
};
// 비밀번호 인풋 (눈 아이콘 토글 동작 확인용)
export const Password: Story = {
args: {
- type: 'password',
labelName: '비밀번호',
- placeholder: '비밀번호를 입력하세요',
- hintMessage: '영문, 숫자, 특수문자를 포함해 주세요.',
required: true,
+ inputProps: {
+ type: 'password',
+ placeholder: '비밀번호를 입력하세요',
+ },
},
};
// 에러 상태 예시 (스타일은 프로젝트 스타일에 맞춰 조정)
export const ErrorState: Story = {
args: {
- type: 'text',
labelName: '닉네임',
- placeholder: '닉네임을 입력하세요',
hintMessage: '이미 사용 중인 닉네임입니다.',
required: true,
- className: ' [&>input]:border-red-500', // 필요에 따라 Input에 에러 스타일 적용
+ inputProps: {
+ type: 'text',
+ placeholder: '닉네임을 입력하세요',
+ },
},
};
diff --git a/src/components/shared/form-input/index.tsx b/src/components/shared/form-input/index.tsx
index a2d96e90..b50abc69 100644
--- a/src/components/shared/form-input/index.tsx
+++ b/src/components/shared/form-input/index.tsx
@@ -1,59 +1,78 @@
'use client';
-import { useState } from 'react';
+import { InputHTMLAttributes, useId, useState } from 'react';
import { Icon } from '@/components/icon';
import { Hint, Input, Label } from '@/components/ui';
import { cn } from '@/lib/utils';
-interface FormInputProps extends React.InputHTMLAttributes {
+interface PasswordToggleButtonProps {
+ isVisible: boolean;
+ onToggle: () => void;
+}
+
+const PasswordToggleButton = ({ isVisible, onToggle }: PasswordToggleButtonProps) => {
+ return (
+
+ );
+};
+
+interface FormInputProps {
+ className?: string;
labelName?: string;
hintMessage?: string;
+ required?: boolean;
+ inputProps?: InputHTMLAttributes;
}
export const FormInput = ({
className,
- required,
labelName,
hintMessage,
- type,
- id,
- ...inputProps
+ required = true,
+ inputProps = {},
}: FormInputProps) => {
+ const { type = 'text', id, required: _, ...restInputProps } = inputProps;
+
+ const generatedId = useId();
const [isVisible, setIsVisible] = useState(false);
- const isPasswordField = type === 'password';
- const inputType = isVisible ? 'text' : type;
- const inputId = id ?? `form-input-${labelName ?? 'field'}`;
+ const isPasswordField = type === 'password';
+ const inputType = isPasswordField && isVisible ? 'text' : type;
+ const inputId = id ?? generatedId;
const handleToggle = () => {
- if (!isPasswordField) return;
setIsVisible((prev) => !prev);
};
- const passwordIconButton = isPasswordField ? (
-
- ) : null;
-
return (
+ ) : undefined
+ }
required={required}
type={inputType}
- {...inputProps}
+ {...restInputProps}
/>
{hintMessage && }
diff --git a/src/components/shared/search-bar/index.stories.tsx b/src/components/shared/search-bar/index.stories.tsx
index d14295cb..6781ebb8 100644
--- a/src/components/shared/search-bar/index.stories.tsx
+++ b/src/components/shared/search-bar/index.stories.tsx
@@ -1,42 +1,62 @@
-// SearchBar.stories.tsx
+import { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/nextjs';
import { SearchBar } from './index';
-const meta = {
+const meta: Meta = {
title: 'Components/SearchBar',
component: SearchBar,
tags: ['autodocs'],
- parameters: {
- layout: 'centered',
- },
argTypes: {
- onSearch: { action: 'searched' },
- onChange: { action: 'changed' },
- },
- args: {
- placeholder: '검색어를 입력하세요',
+ placeholder: { control: 'text' },
+ defaultValue: { control: 'text' },
+ className: { control: 'text' },
+ onSearch: { action: 'search' },
+ onChange: { action: 'change' },
},
-} satisfies Meta;
+};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
-// 기본 검색바
-export const Default: Story = {};
+// 기본: 내부 상태(uncontrolled)로 동작
+export const Default: Story = {
+ args: {
+ placeholder: '원하는 모임을 검색해보세요.',
+ },
+};
-// 넓은 너비를 가진 검색바 예시
-export const FullWidth: Story = {
+// 초기값이 있는 uncontrolled 사용 예시
+export const WithDefaultValue: Story = {
args: {
- className: 'w-[440px]',
+ placeholder: '검색어를 입력하세요.',
+ defaultValue: '초기 검색어',
},
};
-// 긴 placeholder 예시
-export const WithLongPlaceholder: Story = {
+// 부모가 value를 제어하는 controlled 사용 예시
+export const Controlled: Story = {
args: {
- placeholder: '검색어를 입력한 뒤 엔터를 누르거나 아이콘을 클릭하세요',
+ placeholder: '제어형 검색 바',
+ value: '초기 값',
+ },
+ render: (args) => {
+ const [value, setValue] = useState(args.value ?? '');
+
+ return (
+ {
+ setValue(nextValue);
+ // Storybook action(onChange)도 같이 호출
+ if (typeof args.onChange === 'function') {
+ (args.onChange as (value: string) => void)(nextValue);
+ }
+ }}
+ />
+ );
},
};
diff --git a/src/components/shared/search-bar/index.tsx b/src/components/shared/search-bar/index.tsx
index c8796991..8c332f4e 100644
--- a/src/components/shared/search-bar/index.tsx
+++ b/src/components/shared/search-bar/index.tsx
@@ -1,54 +1,77 @@
'use client';
-import { useState } from 'react';
+import { ChangeEvent, InputHTMLAttributes, KeyboardEvent, useState } from 'react';
import { Icon } from '@/components/icon';
import { Input } from '@/components/ui';
import { cn } from '@/lib/utils';
-interface SearchBarProps extends React.InputHTMLAttributes {
+interface SearchBarProps extends Omit, 'value' | 'onChange'> {
onSearch?: (value: string) => void;
+ onChange?: (value: string) => void;
+ value?: string;
+ defaultValue?: string;
}
export const SearchBar = ({
className,
- placeholder,
+ placeholder = '검색어를 입력하세요',
onSearch,
onChange,
+ value,
+ defaultValue = '',
...props
}: SearchBarProps) => {
- const [value, setValue] = useState('');
+ const isControlled = value !== undefined;
- const handleChange = (event: React.ChangeEvent) => {
- setValue(event.target.value);
- onChange?.(event);
+ const [innerValue, setInnerValue] = useState(defaultValue);
+
+ const currentValue = isControlled ? value : innerValue;
+
+ const handleChange = (event: ChangeEvent) => {
+ const newValue = event.target.value;
+
+ if (!isControlled) {
+ setInnerValue(newValue);
+ }
+
+ onChange?.(newValue);
};
- const handleIconClick = () => {
- onSearch?.(value);
+ const handleSearch = () => {
+ onSearch?.(currentValue ?? '');
+
+ // 추가 동작 추천?
+ // 검색 실행 전 공백 제거 로직
};
- const searchIconButton = (
-
- );
+ const handleKeyDown = (event: KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ handleSearch();
+ }
+ };
return (
-
-
-
+
+
+
+ }
+ placeholder={placeholder}
+ value={currentValue}
+ onChange={handleChange}
+ onKeyDown={handleKeyDown}
+ {...props}
+ />
);
};
diff --git a/src/components/ui/input/index.stories.tsx b/src/components/ui/input/index.stories.tsx
index 84735b7a..0fec2544 100644
--- a/src/components/ui/input/index.stories.tsx
+++ b/src/components/ui/input/index.stories.tsx
@@ -1,93 +1,61 @@
-// Input.stories.tsx
-
import type { Meta, StoryObj } from '@storybook/nextjs';
import { Icon } from '@/components/icon';
import { Input } from './index';
-const meta = {
+const meta: Meta = {
title: 'Components/Input',
component: Input,
tags: ['autodocs'],
- parameters: {
- layout: 'centered',
- },
argTypes: {
+ placeholder: { control: 'text' },
type: {
- control: 'text',
- },
- placeholder: {
- control: 'text',
- },
- disabled: {
- control: 'boolean',
+ control: { type: 'select' },
+ options: ['text', 'email', 'password', 'search'],
},
- className: {
- control: 'text',
- },
- },
- args: {
- placeholder: '값을 입력하세요',
- type: 'text',
+ disabled: { control: 'boolean' },
+ className: { control: 'text' },
},
-} satisfies Meta;
+};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
-// 기본 인풋
+// 기본 텍스트 인풋
export const Default: Story = {
- render: (args) => (
-
-
-
- ),
+ args: {
+ placeholder: '텍스트를 입력하세요',
+ type: 'text',
+ },
};
// 비밀번호 인풋
export const Password: Story = {
args: {
- type: 'password',
placeholder: '비밀번호를 입력하세요',
+ type: 'password',
},
- render: (args) => (
-
-
-
- ),
};
-// 오른쪽에 검색 아이콘 버튼이 있는 인풋
-export const WithSearchIcon: Story = {
+// 검색 아이콘이 포함된 인풋
+export const WithIconButton: Story = {
args: {
- type: 'search',
placeholder: '검색어를 입력하세요',
+ type: 'search',
+ iconButton: (
+
+ ),
},
- render: (args) => (
-
-
-
-
- }
- />
-
- ),
};
// 비활성화된 인풋
export const Disabled: Story = {
args: {
+ placeholder: '입력할 수 없습니다',
disabled: true,
- placeholder: '비활성 상태',
},
- render: (args) => (
-
-
-
- ),
};
diff --git a/src/components/ui/input/index.tsx b/src/components/ui/input/index.tsx
index 9c30ae24..f57b889f 100644
--- a/src/components/ui/input/index.tsx
+++ b/src/components/ui/input/index.tsx
@@ -15,8 +15,8 @@ export const Input = forwardRef(
{
export const Label = ({ children, className, required, ...props }: LabelProps) => {
return (
+ // label의 required는 부르는 곳에서 제어