인풋 공톰 컴포넌트 구현#13
Conversation
Summary of ChangesHello @jungwon123, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! 이 PR은 애플리케이션 전반에 걸쳐 사용될 공통 인풋 컴포넌트 세트를 도입하여 사용자 인터페이스의 일관성을 강화하고 개발 효율성을 높이는 것을 목표로 합니다. 다양한 입력 필드에 대한 표준화된 솔루션을 제공함으로써, 향후 개발에서 재사용성을 증대시키고 디자인 시스템의 통일성을 유지할 수 있습니다. Highlights
🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console. Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
전반적으로 공통 인풋 컴포넌트들을 체계적으로 잘 구현해주셨습니다. 몇 가지 중요한 수정사항과 개선점을 제안드립니다. 특히, ref를 컴포넌트에 전달하기 위한 React.forwardRef의 사용법을 확인해주시면 좋겠습니다. 이는 react-hook-form과 같은 라이브러리와의 통합에 필수적입니다. 또한, 웹 접근성 향상을 위해 <label>과 <input>을 연결하는 부분과 사용자 경험을 개선할 수 있는 몇 가지 사항에 대한 피드백을 포함했습니다.
| import clsx from 'clsx'; | ||
| import { InputProps } from './types/types'; | ||
| import styles from './styles/Input.module.css'; | ||
|
|
||
| /** | ||
| * 공통 Input 컴포넌트. | ||
| * @param className 추가 CSS 클래스 | ||
| * @param ref 외부에서 전달하는 ref (react-hook-form 등) | ||
| * @param props 네이티브 input의 모든 속성(placeholder, type, onChange 등) | ||
| */ | ||
| export default function Input({ className, ref, ...props }: InputProps) { | ||
| return <input ref={ref} className={clsx(styles.input, className)} {...props} />; | ||
| } |
There was a problem hiding this comment.
함수 컴포넌트에서 ref를 props로 직접 받아 전달할 수 없습니다. ref를 올바르게 전달하기 위해서는 React.forwardRef를 사용해야 합니다. react-hook-form과 같은 라이브러리에서 ref를 사용하려면 이 수정이 필수적입니다.
| import clsx from 'clsx'; | |
| import { InputProps } from './types/types'; | |
| import styles from './styles/Input.module.css'; | |
| /** | |
| * 공통 Input 컴포넌트. | |
| * @param className 추가 CSS 클래스 | |
| * @param ref 외부에서 전달하는 ref (react-hook-form 등) | |
| * @param props 네이티브 input의 모든 속성(placeholder, type, onChange 등) | |
| */ | |
| export default function Input({ className, ref, ...props }: InputProps) { | |
| return <input ref={ref} className={clsx(styles.input, className)} {...props} />; | |
| } | |
| import { forwardRef } from 'react'; | |
| import clsx from 'clsx'; | |
| import { InputProps } from './types/types'; | |
| import styles from './styles/Input.module.css'; | |
| /** | |
| * 공통 Input 컴포넌트. | |
| * @param className 추가 CSS 클래스 | |
| * @param props 네이티브 input의 모든 속성(placeholder, type, onChange 등) | |
| */ | |
| const Input = forwardRef<HTMLInputElement, InputProps>(({ className, ...props }, ref) => { | |
| return <input ref={ref} className={clsx(styles.input, className)} {...props} />; | |
| }); | |
| Input.displayName = 'Input'; | |
| export default Input; |
| 'use client'; | ||
|
|
||
| import { useState } from 'react'; | ||
| import Image from 'next/image'; | ||
| import clsx from 'clsx'; | ||
| import Input from './Input'; | ||
| import { PasswordInputProps } from './types/types'; | ||
| import visibilityTrue from '@/assets/icons/visibility/visibillityTrue.svg'; | ||
| import visibilityFalse from '@/assets/icons/visibility/visibillityFalse.svg'; | ||
| import styles from './styles/PasswordInput.module.css'; | ||
|
|
||
| /** | ||
| * 비밀번호 Input 컴포넌트. | ||
| * @param className 추가 CSS 클래스 | ||
| * @param ref 외부에서 전달하는 ref (react-hook-form 등) | ||
| * @param props 네이티브 input의 모든 속성(type 제외) | ||
| */ | ||
| export default function PasswordInput({ className, ref, ...props }: PasswordInputProps) { | ||
| const [showPassword, setShowPassword] = useState(false); | ||
|
|
||
| return ( | ||
| <div className={styles.wrapper}> | ||
| <Input | ||
| ref={ref} | ||
| type={showPassword ? 'text' : 'password'} | ||
| className={clsx(styles.input, className)} | ||
| {...props} | ||
| /> | ||
| <button | ||
| type="button" | ||
| className={styles.toggleButton} | ||
| onClick={() => setShowPassword((prev) => !prev)} | ||
| aria-label={showPassword ? '비밀번호 숨기기' : '비밀번호 보기'} | ||
| > | ||
| <Image | ||
| src={showPassword ? visibilityTrue : visibilityFalse} | ||
| alt="" | ||
| width={24} | ||
| height={24} | ||
| /> | ||
| </button> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
부모 컴포넌트로부터 ref를 받아 내부 Input 컴포넌트로 전달하려면 PasswordInput 컴포넌트도 React.forwardRef로 감싸야 합니다. 이는 react-hook-form과 같은 라이브러리와의 연동을 위해 중요합니다.
'use client';
import { forwardRef, useState } from 'react';
import Image from 'next/image';
import clsx from 'clsx';
import Input from './Input';
import { PasswordInputProps } from './types/types';
import visibilityTrue from '@/assets/icons/visibility/visibillityTrue.svg';
import visibilityFalse from '@/assets/icons/visibility/visibillityFalse.svg';
import styles from './styles/PasswordInput.module.css';
/**
* 비밀번호 Input 컴포넌트.
* @param className 추가 CSS 클래스
* @param props 네이티브 input의 모든 속성(type 제외)
*/
const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
({ className, ...props }, ref) => {
const [showPassword, setShowPassword] = useState(false);
return (
<div className={styles.wrapper}>
<Input
ref={ref}
type={showPassword ? 'text' : 'password'}
className={clsx(styles.input, className)}
{...props}
/>
<button
type="button"
className={styles.toggleButton}
onClick={() => setShowPassword((prev) => !prev)}
aria-label={showPassword ? '비밀번호 숨기기' : '비밀번호 보기'}
>
<Image
src={showPassword ? visibilityTrue : visibilityFalse}
alt=""
width={24}
height={24}
/>
</button>
</div>
);
},
);
PasswordInput.displayName = 'PasswordInput';
export default PasswordInput;
| import clsx from 'clsx'; | ||
| import { TextAreaProps } from './types/types'; | ||
| import styles from './styles/TextArea.module.css'; | ||
|
|
||
| /** | ||
| * 멀티라인 입력 컴포넌트. | ||
| * @param className 추가 CSS 클래스 | ||
| * @param ref 외부에서 전달하는 ref (react-hook-form 등) | ||
| * @param props 네이티브 textarea의 모든 속성(placeholder, rows, onChange 등) | ||
| */ | ||
| export default function TextArea({ className, ref, ...props }: TextAreaProps) { | ||
| return <textarea ref={ref} className={clsx(styles.textarea, className)} {...props} />; | ||
| } |
There was a problem hiding this comment.
함수 컴포넌트에서 ref를 props로 직접 받아 전달할 수 없습니다. ref를 올바르게 전달하기 위해서는 React.forwardRef를 사용해야 합니다. react-hook-form과 같은 라이브러리에서 ref를 사용하려면 이 수정이 필수적입니다.
| import clsx from 'clsx'; | |
| import { TextAreaProps } from './types/types'; | |
| import styles from './styles/TextArea.module.css'; | |
| /** | |
| * 멀티라인 입력 컴포넌트. | |
| * @param className 추가 CSS 클래스 | |
| * @param ref 외부에서 전달하는 ref (react-hook-form 등) | |
| * @param props 네이티브 textarea의 모든 속성(placeholder, rows, onChange 등) | |
| */ | |
| export default function TextArea({ className, ref, ...props }: TextAreaProps) { | |
| return <textarea ref={ref} className={clsx(styles.textarea, className)} {...props} />; | |
| } | |
| import { forwardRef } from 'react'; | |
| import clsx from 'clsx'; | |
| import { TextAreaProps } from './types/types'; | |
| import styles from './styles/TextArea.module.css'; | |
| /** | |
| * 멀티라인 입력 컴포넌트. | |
| * @param className 추가 CSS 클래스 | |
| * @param props 네이티브 textarea의 모든 속성(placeholder, rows, onChange 등) | |
| */ | |
| const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(({ className, ...props }, ref) => { | |
| return <textarea ref={ref} className={clsx(styles.textarea, className)} {...props} />; | |
| }); | |
| TextArea.displayName = 'TextArea'; | |
| export default TextArea; |
| import { ComponentPropsWithRef, ReactNode } from 'react'; | ||
|
|
||
| export type InputProps = ComponentPropsWithRef<'input'>; | ||
|
|
||
| export type PasswordInputProps = Omit<ComponentPropsWithRef<'input'>, 'type'>; | ||
|
|
||
| export type TextAreaProps = ComponentPropsWithRef<'textarea'>; | ||
|
|
||
| export type PasswordFieldProps = Omit<ComponentPropsWithRef<'input'>, 'type'>; | ||
|
|
||
| export type ActionTextAreaProps = TextAreaProps & { | ||
| onSubmit?: () => void; | ||
| wrapperClassName?: string; | ||
| }; | ||
|
|
||
| export type CommentInputProps = Omit<ActionTextAreaProps, 'wrapperClassName'>; | ||
|
|
||
| export type AccountInputProps = { | ||
| email?: string; | ||
| children?: ReactNode; | ||
| }; | ||
|
|
||
| export type ChangePasswordProps = { | ||
| isEditing?: boolean; | ||
| newPasswordProps?: PasswordFieldProps; | ||
| confirmPasswordProps?: PasswordFieldProps; | ||
| children?: ReactNode; | ||
| }; |
There was a problem hiding this comment.
여러 타입을 정리하고 ref 전달 문제를 해결하기 위해 다음과 같이 수정을 제안합니다:
ComponentPropsWithRef대신ComponentProps를 사용하여forwardRef와 함께 사용하기 좋게 만듭니다.ref는forwardRef가 별도로 처리해줍니다.- 중복되는
PasswordFieldProps타입을 제거하고PasswordInputProps로 통일합니다.
import { ComponentProps, ReactNode } from 'react';
export type InputProps = ComponentProps<'input'>;
export type PasswordInputProps = Omit<ComponentProps<'input'>, 'type'>;
export type TextAreaProps = ComponentProps<'textarea'>;
export type ActionTextAreaProps = TextAreaProps & {
onSubmit?: () => void;
wrapperClassName?: string;
};
export type CommentInputProps = Omit<ActionTextAreaProps, 'wrapperClassName'>;
export type AccountInputProps = {
email?: string;
children?: ReactNode;
};
export type ChangePasswordProps = {
isEditing?: boolean;
newPasswordProps?: PasswordInputProps;
confirmPasswordProps?: PasswordInputProps;
children?: ReactNode;
};| import Input from './Input'; | ||
| import { ChangePasswordProps } from './types/types'; | ||
| import styles from './styles/ChangePassword.module.css'; | ||
|
|
||
| /** | ||
| * 비밀번호 변경 컴포넌트. | ||
| * @param isEditing 편집 모드 여부 (false면 인풋 비활성화, 기본값 false) | ||
| * @param newPasswordProps 새 비밀번호 인풋에 전달할 props | ||
| * @param confirmPasswordProps 새 비밀번호 확인 인풋에 전달할 props | ||
| * @param children 버튼 등 하단 영역에 렌더링할 요소 | ||
| */ | ||
| export default function ChangePassword({ | ||
| isEditing = false, | ||
| newPasswordProps, | ||
| confirmPasswordProps, | ||
| children, | ||
| }: ChangePasswordProps) { | ||
| return ( | ||
| <div className={styles.container}> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>새 비밀번호</label> | ||
| <Input | ||
| type="password" | ||
| placeholder="새 비밀번호를 입력해 주세요." | ||
| disabled={!isEditing} | ||
| {...newPasswordProps} | ||
| /> | ||
| </div> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>새 비밀번호 확인</label> | ||
| <Input | ||
| type="password" | ||
| placeholder="새 비밀번호를 다시 한 번 입력해 주세요." | ||
| disabled={!isEditing} | ||
| {...confirmPasswordProps} | ||
| /> | ||
| </div> | ||
| <div className={styles.buttonArea}>{children}</div> | ||
| </div> | ||
| ); | ||
| } |
There was a problem hiding this comment.
ChangePassword 컴포넌트를 개선할 몇 가지 제안사항이 있습니다:
PasswordInput사용: 현재Input컴포넌트를 직접 사용하고 있는데, 비밀번호 가시성 토글 기능이 있는PasswordInput컴포넌트를 사용하면 사용자 경험이 향상됩니다.- 웹 접근성:
<label>과<input>을htmlFor와id속성으로 연결하여 스크린 리더 사용자들이 더 쉽게 양식을 이용할 수 있도록 해야 합니다.
import PasswordInput from './PasswordInput';
import { ChangePasswordProps } from './types/types';
import styles from './styles/ChangePassword.module.css';
/**
* 비밀번호 변경 컴포넌트.
* @param isEditing 편집 모드 여부 (false면 인풋 비활성화, 기본값 false)
* @param newPasswordProps 새 비밀번호 인풋에 전달할 props
* @param confirmPasswordProps 새 비밀번호 확인 인풋에 전달할 props
* @param children 버튼 등 하단 영역에 렌더링할 요소
*/
export default function ChangePassword({
isEditing = false,
newPasswordProps,
confirmPasswordProps,
children,
}: ChangePasswordProps) {
return (
<div className={styles.container}>
<div className={styles.field}>
<label className={styles.label} htmlFor="new-password">새 비밀번호</label>
<PasswordInput
id="new-password"
placeholder="새 비밀번호를 입력해 주세요."
disabled={!isEditing}
{...newPasswordProps}
/>
</div>
<div className={styles.field}>
<label className={styles.label} htmlFor="confirm-password">새 비밀번호 확인</label>
<PasswordInput
id="confirm-password"
placeholder="새 비밀번호를 다시 한 번 입력해 주세요."
disabled={!isEditing}
{...confirmPasswordProps}
/>
</div>
<div className={styles.buttonArea}>{children}</div>
</div>
);
}
| <div className={styles.field}> | ||
| <label className={styles.label}>이메일</label> | ||
| <Input type="email" value={email} disabled className={styles.readOnly} /> | ||
| </div> | ||
| <div className={styles.field}> | ||
| <label className={styles.label}>비밀번호</label> | ||
| <Input type="password" value="••••••••" disabled className={styles.readOnly} /> | ||
| </div> |
There was a problem hiding this comment.
두 가지 개선점을 제안합니다:
- 웹 접근성: 스크린 리더 사용자의 편의를 위해
<label>의htmlFor속성과<input>의id속성을 연결하여 접근성을 향상시켜야 합니다. - 제어되지 않는 컴포넌트 오류 방지:
emailprop이undefined일 경우, React에서 제어되지 않는(uncontrolled) input이 제어되는(controlled) input으로 변경된다는 경고가 발생할 수 있습니다.value={email ?? ''}와 같이 기본값을 제공하여 이를 방지할 수 있습니다.
| <div className={styles.field}> | |
| <label className={styles.label}>이메일</label> | |
| <Input type="email" value={email} disabled className={styles.readOnly} /> | |
| </div> | |
| <div className={styles.field}> | |
| <label className={styles.label}>비밀번호</label> | |
| <Input type="password" value="••••••••" disabled className={styles.readOnly} /> | |
| </div> | |
| <div className={styles.field}> | |
| <label htmlFor="account-email" className={styles.label}>이메일</label> | |
| <Input id="account-email" type="email" value={email ?? ''} disabled className={styles.readOnly} /> | |
| </div> | |
| <div className={styles.field}> | |
| <label htmlFor="account-password" className={styles.label}>비밀번호</label> | |
| <Input id="account-password" type="password" value="••••••••" disabled className={styles.readOnly} /> | |
| </div> |
| rows={1} | ||
| className={clsx(styles.textarea, className)} | ||
| onChange={(e) => { | ||
| setHasValue(e.target.value.length > 0); |
Summary
atoms 와 molecules로 작업을 하다가 통일성이 안맞아서 처음 pull 해온 컴포넌트 폴더 형식에 맞추어 작업했습니다.


제가 정의했던 인풋 atom은 아래 이미지 2개이고 나머지는 아이콘, 버튼 조합의 molecule로 생각했습니다.
구현된 컴포넌트
Issue
#4