Skip to content

Conversation

@WonJuneKim
Copy link
Contributor

@WonJuneKim WonJuneKim commented Nov 1, 2025

💡 작업 내용

  • textField 구현
  • selectField 구현
  • tagField(어드민) 구현
  • Select 구현
  • InputArea 구현
  • Label 일부 수정

💡 자세한 설명

✅ Select

  • SelectField 및 다른 컴포넌트들의 기반이 되는 Select 컴포넌트를 구현하였습니다.

  • Context를 사용하여 구현하였으며, 자식 요소로는SelectLabel , SelectCheckBox, SelectRadio를 가집니다. 따라서 Label, Radio, Checkbox는 모두 Consumer로 사용됩니다.

  • 단일 Select 컨테이너 + variant prop으로 동작 결정하며, Context는 Select.tsx에서만 제공합니다.

  • 현재 Select는 checkbox와 radio가 구현 전이기에 SelectLabel 만 연동을 할 수 있는 상태입니다.

✅ Input 공통

  • 디자인 에셋에서 Field 형태인 TextField, TagField, SelectField , Area 형태인 InputArea로 구성됩니다.

  • 공통적인 스타일링은 shared를 통해 분리하였고, 인터페이스는 별도로 선언하였습니다. (인터페이스도 기본 >> 확장형으로 구현했다면 더 깔꼼했을 거 같네요.)

  • 디자인 에셋에서는 BlockButton이 주입되어있는 형태가 기본형이지만, TextField, TextFieldButton과 같이 버튼의 경우 기본형을 확장한 형태로 구현하였습니다.

  • 각 Input들을 최대한 controlled Pattern을 이용하여 구현하였습니다. 각 컴포넌트는 defaultValue를 가지지 않으며, 외부에서 onChange를 통해 사용자 액션을 감지하고, 업데이트 된 state에 따라 작동합니다.

✅ SelectField 관련

export const SelectField = forwardRef<HTMLDivElement, SelectFieldProps>(
  (
    {
      style = 'outlined',
      layout = 'vertical',
      validation = 'none',
      interaction = 'enabled',
      label,
      labelIcon,
      helperText,
      value,
      placeholder = '플레이스 홀더 텍스트',
      dropdownIcon = 'arrow-down-s-fill',
      isOpen = false,
      onClick,
      children,
      ...restProps
    },
    ref,
  ) => {

와 같이 Select 호출과 열림 상태를 관리할 때 isOpen 그리고 onClick 두가지 상태로 관리합니다. 따라서 해당 방식은 완전한 Controlled Pattern이 아닙니다.

문제
SelectField가 직접적으로 Select를 import하지 않고 children으로 가능성을 열어두었기 때문에 onChange props가 있어도 사용처가 없음

임시 해결책 1 (이후 삭제됨)
SelectField 역시 Select의 Context 내부에서 처리되는 방식

현재 해결책
컴포넌트가 단순 display에만 영향을 끼치도록 2가지 props(isOpen, onClick) 유지

  • SelectField가 완전히 Select 컴포넌트 종속적으로 생각한다면 Select 를 import하거나 context안에서 다시 처리해야하는 것이 제일 이상적인 상황이라고 생각합니다.

✅ TagField

  • TagField의 경우 내부에서 처리되어야하는 키보드 액션 방식들이 존재합니다.

  • 최대한 순수 함수를 이용해서 기본 동작들을 선언하였고, (TagField.utils) Tag관련 상태(순수한 상태, 비즈니스 로직 제외)는 커스텀 훅을 사용합니다. (useTagFieldState)

✅ Label 컴포넌트 변경점

  • Label의 경우 <label> 외에도 <span> 등과 같은 다양한 스타일로 사용하는 케이스가 있습니다. 이러한 다형성 처리를 위해 Emotion의 as를 사용하였습니다.

  • 관련 설명은 주석 및 TsDocs로 기재해놓았습니다.

type LabelElement = HTMLLabelElement | HTMLSpanElement | HTMLDivElement | HTMLParagraphElement;

export interface LabelProps extends ComponentPropsWithoutRef<'label'> {
  as?: 'label' | 'span' | 'div' | 'p';
  size?: LabelSize;
  textAlign?: LabelTextAlign;
  weight?: LabelWeight;
  color?: string;
  children: ReactNode;
}

와 같이 as로 사용할 수 있는 메타 태그를 명시적으로 선언하였습니다.

  • 트레이드 오프 : ref의 경우 사용하는 태그의 케이스에 따라서 ref 타입 단언이 필요하게 됩니다. 런타임과는 다르게 typescript 내부에서는 ref={ref as Ref<HTMLLabelElement>} 로 처리하였습니다.

📗 참고 자료 (선택)

📢 리뷰 요구 사항 (선택)

  • SelectField 관련 추후 구현 방향성

✅ 셀프 체크리스트

  • 머지할 브랜치 확인했나요?
  • 이슈는 close 했나요?
  • Reviewers, Labels, Projects를 등록했나요?
  • 기능이 잘 동작하나요?
  • 불필요한 코드는 제거했나요?

closes #240

- emotion의 as prop을 통해 다양한 HTML 요소로 렌더링 가능하도록 수정
- shouldForwardRef를 이용해서 스타일링에서만 사용되는 props는 드러나지 않도록 합니다.
- 기본형은 TextField
- 버튼이 들어가 있는 구조는 TextFieldButton
- (추가) shouldForwardProp으로 검증하는 값은 최하위 컴포넌트 단위에서 수행하도록 변경
- children으로 Select 또는 이외의 자식 요소를 받음
- 이러한 처리로 인해 isOpen과 onClick 두 props로 같은 상태를 제어하는 문제 >> Select Context 안에 Select.Field 를 추가 생성
…ent into feat/240-input-design-system

# Conflicts(참고용):
#	packages/jds/src/components/Badge/contentBadge/ContentBadge.tsx
#	packages/jds/src/components/Badge/numericBadge/NumericBadge.tsx
#	packages/jds/src/components/Image/Image.style.ts
#	packages/jds/src/components/Label/Label.style.ts
#	packages/jds/src/components/Label/Label.tsx
#	packages/jds/src/components/index.ts
Copy link
Contributor

@rkdcodus rkdcodus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

수정된 부분도 확인했습니다! 👍👍
의견은 코멘트 달아두었고 충돌 부분만 확인되면 될 것 같습니다 고생하셨습니다!

resize: $hasFixedHeight ? 'none' : 'vertical',
overflow: 'auto',
boxSizing: 'border-box',
fieldSizing: 'content',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하~ 이런 속성이 있었군요 자동 사이즈 확장 속성이네요!! 편리하네요 👍
아직 사파리나 firefox 같은 곳에서는 지원하진 않는 속성이라고 하네요 지원하지 않는 브라우저에는 대체 로직이 필요할 것 같아요! 일단 일정 상 TODO로 남겨두는 것도 괜찮을 것 같습니다

Copy link
Contributor Author

@WonJuneKim WonJuneKim Nov 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fieldSizing은 말씀해주신 거처럼 experimental technology 입니다.

d249879 에 명시해두엇서요!

Comment on lines 141 to 163
export const DynamicHeight: Story = {
args: {
label: '동적 높이',
minHeight: 150,
helperText: '최소 150px이며, 내용에 따라 자동으로 늘어납니다',
maxLength: 1000,
value: '',
onChange: () => {},
},
render: function Render(args) {
const [value, setValue] = useState('');
return (
<div style={{ width: '560px' }}>
<InputArea
{...args}
value={value}
onChange={e => setValue(e.target.value)}
placeholder='여러 줄을 입력해보세요. 자동으로 높이가 늘어납니다.'
/>
</div>
);
},
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아하~ 확인했습니다 fieldSizing이라는 속성이 있었군요 자동 사이즈 확장 속성이네요!! 편리하네요 👍
아직 사파리나 firefox 같은 곳에서는 지원하진 않는 속성이라고 하네요 지원하지 않는 브라우저에는 대체 로직이 필요할 것 같아요! 일단 일정 상 TODO로 남겨두는 것도 괜찮을 것 같습니다

>
{label}
</StyledFieldLabel>
{labelIcon && <Icon name={labelIcon} size='2xs' />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵! 네이밍은 자율로 가면 될 것 같아요!
그리고 아이콘 색상은 제가 확인한 바로 디자인에서 레이블과 아이콘이 색상이 다르더라구요 재확인 부탁드립니다!


export type InputAreaStyle = 'outlined' | 'empty';
export type InputAreaLayout = 'vertical' | 'horizontal';
export type InputAreaValidation = 'none' | 'error';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아 이거 제가 field랑 헷갈렸네요 ㅜㅜ 확인했습니다!

Comment on lines 105 to 108
<StyledFieldContainer $layout={layout || 'vertical'}>
<FormFieldLabel />
<FormFieldContent>{children}</FormFieldContent>
</StyledFieldContainer>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

폼 필드 분리해주니까 구분이 되어서 좋네요!! 이대로 가도 문제는 없을 것 같습니다!
그렇지만 ui 구조랑 폼필드 결합도가 좀 높아보여요. 폼 필드 안에 Lable이랑 Content랑 helpText 위치가 고정된 형태인데 FormField는 Context Provider만 담당하고 해당 Field 컴포넌트에서 조립식으로 할 수 있게끔 (슬롯 구조로) 바꾸는 것이 유연한 방식이라 생각합니다!

<FormField>
  <StyledFieldContainer>
    <FormField.Label />
    <FormField.Content>
      <SelectFieldInput />
    </FormField.Content>
    <FormField.HelperText />
  </StyledFieldContainer>
</FormField>

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f86c76b 저도 채연님 말씀에 동의합니다~

강한 결합도를 분리시키고, 기존 SelectField, TagField, TextField는 추상화 계층으로 사용하였습니다!

Copy link
Contributor

@rkdcodus rkdcodus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

변경 사항 확인했습니다! 👍👍

@WonJuneKim WonJuneKim merged commit 1f53df3 into dev Nov 20, 2025
4 of 5 checks passed
@WonJuneKim WonJuneKim deleted the feat/240-input-design-system branch November 20, 2025 10:05
whdgur5717 pushed a commit that referenced this pull request Nov 21, 2025
* refactor: Label 컴포넌트의 태그를 label로 변경합니다.
- emotion의 as prop을 통해 다양한 HTML 요소로 렌더링 가능하도록 수정

* refactor: theme이 컴포넌트가 아닌 스타일 단위에서 처리될 수 있도록 변경합니다.
- shouldForwardRef를 이용해서 스타일링에서만 사용되는 props는 드러나지 않도록 합니다.

* refactor: 수정된 Label 컴포넌트를 Badge에 적용합니다.

* feat: Label props를 export할 수 있도록 선언합니다.

* feat: Context를 사용하여 Select 컴포넌트를 구현합니다.

* feat: Select 컴포넌트의 variant에 따라 분기되는 자식 요소를 구현합니다.

* feat: Select와 자식 요소들을 스타일링합니다.

* feat: 순수 함수 구조의 select 상태를 관리하는 핸들러를 생성합니다.

* test: Select 컴포넌트의 스토리북 테스트 코드를 작성합니다.

* feat: select 컴포넌트와 인터페이스를 export합니다.

* feat: input 컴포넌트 중 TextField 컴포넌트를 작성합니다.
- 기본형은 TextField
- 버튼이 들어가 있는 구조는 TextFieldButton

* feat: textField의 인터페이스를 선언합니다.

* feat: textField의 스타일링 코드를 작성합니다.

* feat: TextField의 구조를 최적화하기 위한 추가 wrapper를 생성합니다.

* feat: TextField 스타일링 시 layout 값에 따라 다른 스타일링을 처리하도록 변경합니다.

- (추가) shouldForwardProp으로 검증하는 값은 최하위 컴포넌트 단위에서 수행하도록 변경

* test: TextField의 스토리북을 작성합니다.

* test: TextField의 요소들을 export 합니다.

* feat: SelectField 컴포넌트를 구현합니다.
- children으로 Select 또는 이외의 자식 요소를 받음
- 이러한 처리로 인해 isOpen과 onClick 두 props로 같은 상태를 제어하는 문제 >> Select Context 안에 Select.Field 를 추가 생성

* feat: SelectField를 스타일링 합니다.

* feat: SelectField의 선택된 값을 추가 스타일링합니다.

* test: SelectField의 스토리북 코드를 작성합니다.

* feat: SelectField의 요소들을 export 합니다.

* feat: 순환참조 문제가 발생할 수 있는 import문(경로가 축소된 import문)을 변경합니다.

* feat: 순환참조 문제가 발생할 수 있는 스타일링 부분을 공용 부분과 공용이 아닌 부분으로 분할합니다.

* feat: Input 컴포넌트에서 공통으로 사용되는 스타일링 부분을 추출합니다.

* feat: TagField 컴포넌트를 구현합니다.

* feat: TagField 컴포넌트를 구현합니다.

* feat: Input 컴포넌트들의 공통되는 타입을 추출합니다.

* feat: 공통 타입 추출에 영향을 받는 부분을 수정합니다.

* feat: disabled와 readOnly를 interaction props로 추상화합니다.

* feat: TagField의 스타일링 코드를 작성합니다.

* test: TagField의 스토리북 코드를 작성합니다.

* feat: input의 스타일 variant별로 interactionLayer의 매개변수를 분리합니다.

* feat: input의 스타일 variant별로 interactionLayer의 매개변수를 분리합니다.

* feat: empty style일 때 레이아웃을 조정합니다.

* feat: empty 스타일일 때의 interactionLayer 매개변수를 명시적으로 분기합니다.

* feat: SelectField에서 Button을 사용하는 확장 컴포넌트를 구현합니다.

* test: SelectField의 스토리북을 Button이 있는 형태가 기본이 되도록 합니다.

* feat: TagField에 버튼이 있는 확장형 컴포넌트를 구현합니다.

* test: TagField의 스토리북에 버튼이 있는 버전을 추가합니다.

* feat: Interaction props의 반환 값 네이밍을 변경합니다.

* feat: Icon의 색상을 별도 함수로 관리합니다.

* feat: 버튼형태가 추가된 Input 컴포넌트들을 export합니다.

* feat: TagField에 사용되는 비즈니스 로직 중 순수함수로 분리할 수 있는 로직을 추출합니다.

* feat: TagField의 상태를 커스텀 훅을 통해서 관리합니다.

* refactor: TagField에 유틸리티 함수와 커스텀 훅을 적용시킵니다.

* feat: InputArea 컴포넌트를 구현합니다.

* feat: InputArea의 스타일링 코드를 작성합니다.

* feat: InputArea의 높이를 props를 통해 조절하도록 변경합니다.

* feat: InputArea 중 textarea의 스타일링 방식을 변경합니다.

* test: InputArea의 스토리북 코드를 작성합니다.

* feat: 구현한 InputArea 컴포넌트를 export합니다.

* feat: Input 관련 컴포넌트를 한번에 export합니다.

* feat: component 단위에서 구현한 컴포넌트를 export합니다.

* feat: 수정된 토큰 네이밍에 맞춰서 스타일링 토큰 값을 변경합니다.

* feat: 수정된 토큰 네이밍에 맞춰서 스타일링 토큰 값을 변경합니다.

* feat: InputArea가 기본적으로 유동적인 height를 가지도록 변경합니다.
- min-height는 최소한의 크기를 보장해줌
- 특정 높이가 필요할 경우 height props로 제어

* feat: props로 받는 height, min-height는 wrapper의 길이에만 영향을 주도록 변경합니다.
- 내부 TextArea의 height는 100% 유지

* test: 변경된 inputArea에 스토리북을 사용해 테스트 케이스를 작성합니다.

* docs: 중복 작성되는 InteractionLayer의 매핑 객체의 개선 방향성을 작성합니다.

* feat: Icon에 색상이 적용되지 않는 문제를 container에 색상을 주입하여 해결합니다.

* feat: 선택 필드에 대한 Aria-role을 combobox로 변경합니다.

* refactor: 폼 입력 필드(Input)를 사용하는 컴포넌트들을 관리하기 위한 Context를 구현합니다.

* refactor: TagField 에서 TagList와 TagItem을 별도의 presentation component로 분리합니다.

* refactor: 각 Input에서 폼 입력 필드를 구독할수 있도록 변경합니다.
- Context로 구성된 FormField의 소비자가 됨

* feat: 변경된 토큰 값을 기존 스타일에 반영합니다.

* feat: 변경된 토큰 값을 기존 스타일에 반영합니다.

* feat: Label 스타일에 HeroStyle을 import하고 있는 구간을 별도 선언으로 분리합니다.

* feat: Icon 컴포넌트의 type을 import해서 props의 type을 명시합니다.

* fix: deprecated된 토큰 타입을 주석 처리합니다.

* refactor: FormField의 결합도를 낮추고, 네이밍된 Field들을 케이스별 프리셋으로 사용합니다.

* feat: 레이블 내 아이콘 스타일링을 변경합니다.

* feat: 레이블 내 아이콘 스타일링을 변경합니다.

* docs: textArea의 개선점을 작성합니다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨feature 구현, 개선 사항 관련 부분 👩🏻‍💻frontend 프론트엔드 작업

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

feat: Input 컴포넌트 - 디자인 시스템을 구현합니다.

3 participants