[TIL]Headless 컴포넌트

이정수·2023년 9월 11일
0

TIL

목록 보기
2/2
post-thumbnail

HeadlessComponent

컴포넌트를 잘만든다 -> 유지보수를 용이하게 한다.

컴포넌트 잘 만들기 (as Headless 컴포넌트)

컴포넌트를 잘 설계하는 방법

<해결하고자 하는 문제>

  1. 재사용 가능한 컴포넌트 만들기
  2. 변경에 따른 부수효과를 최소화 하기

<목표>

  1. 관심사가 분리
  2. 재사용성이 높고 유지보수하기 용이한 컴포넌트 만들기

<컴포넌트의 역할>

  1. 데이터를 관리한다.
    • 외부에서 주입받은 데이터
    • 상태와 같은 내부 데이터
  2. 데이터가 사용자에게 어떻게 보여질지를 정의한다.(UI)
  3. 사용자와 어떻게 상호작용할지를 정의한다.

예제

이름을 입력받는 Input컴포넌트를 작성해보았다.

const SomeInput = () => {
  const [value, setValue] = useState('')
  const handleChangeValue = e => {
    setValue(e.target.value)
  }
  return (
    <div className="input-container">
      <label htmlFor="1">NAME</label>
      <input id="1" type="text" value={value} onChange={handleChangeValue} />
    </div>
  )
}

이 컴포넌트를 위에서 작성한 컴포넌트의 역할에 따라 나눠 보자면

  1. const [value, setValue] = useState('') 내부데이터인 상태
  2. UI를 정의하는 JSX
    <label htmlFor="1">NAME</label>
    <input id="1" type="text" value={value} onChange={handleChangeValue} />
  </div>
  1. UI와 데이터를 연결하는 eventHandler
const handleChangeValue = e => {
  setValue(e.target.value)
}

요구사항에 따라 스타일링도 하고, 잘 동작하는 인풋컴포넌트를 하나 만들었다고 치자.
그런데 다음날 기획자가 변경된 ui디자인을 요구한다면 어떻게 될까?

아까 만든 Someinput컴포넌트를 재사용하기는 어려울것 같다. 그러면

  1. 새로운 컴포넌트를 만들거나. -> 재사용 불가
  2. props로 스타일링 옵션을 내려준다. -> side effect를 초래할 수 있다.

만약 데이터를 다루는 로직과 UI로직을 분리한다면 새로운 UI가 와도 이전에 만든 컴포넌트를 재사용해서 만들수 있지 않을까?

Headless란?

UI -> Head : 컨텐츠를 보여주는 방법
데이터 -> Body: 컨텐츠

데이터만 남긴 컴포넌트를 headless 컴포넌트라고 한다.

< 남길 부분 >

  • input의 value 상태
  • onChange handler
  • 그외 input attributes

< 없앨 부분 >

  • input이 어떻게 보여질지에 대한 UI
1. Compound Component 패턴
import { useState, createContext, useContext } from 'react'
const InputContext = createContext({
  id: '',
  value: '',
  type: 'text',
  onChange: () => {},
})
//context API를 통해서 컴포넌트 내부에서 공유될 상태(데이터)를 정의한다.

const InputWrapper = ({ id, value, type, onChange, children }) => {
  const contextValue = { id, value, type, onChange }
  return (
    <InputContext.Provider value={contextValue}>
      {children}
    </InputContext.Provider>
  )
}
//부모 컴포넌트를 생성하여 위의 contextAPI를 공유한다.

const Input = ({ ...props }) => {
  const { id, value, type, onChange } = useContext(InputContext)
  return (
    <input id={id} value={value} type={type} onChange={onChange} {...props} />
  )
}
const Label = ({ children, ...props }) => {
  const { id } = useContext(InputContext)
  return (
    <label htmlFor={id} {...props}>
      {children}
    </label>
  )
}
//자식 컴포넌트들을 만든다. 이때 자식 컴포넌트 들은 contextAPI를 통해 필요한 상태를 공유받는다.
InputWrapper.Input = Input
InputWrapper.Label = Label
//(선택) 자식 컴포넌트들을 부모 컴포넌트의 프로퍼티로 등록한다.
//명시적으로 InputWrapper와 Input, Label이 부모자식관계라고 나타낼수 있다.

App.js에서 사용시

//APP.js에서 사용시
function App(){
  const [name, setName] = useState('');
  const handleChangeName = (event) => {
    setName(event.target.value);
  }
  return(
    <div>
      <InputWrapper
        id='name'
        value={name}
        type="text"
        onChange = {handleChangeName}
      >
        <InputWrapper.Input />
        <InputWrapper.Label >Name</InputWrapper.Label>
      </InputWrapper>
    </div>
  )
}

사용처에서는 InputWrapper의 하위 컴포넌트들을 자유롭게 볼 수 있고, 위치를 수정하여 마크업을 수정할 수 도 있다.

2. custom Hook 패턴
const App = () => {
  const {value: name, onChange: onChangeName} = useInput();

  return (
    <div className="input-container">
      <label htmlFor="1">NAME</label>
      <input id="1" type="text" value={name} onChange={onChangeName} />
    </div>
  )
}
3. Function as Children Component 패턴

자식에 어떤것이 들어올지 모른다고 가정한다.
InputHeadless 컴포넌트에는 데이터 로직만 같는다. 해당 데이터 로직을 자식 함수에 주입한다.

const InputHeadless = ({children}) => {
  const [value, setValue] = useState('');

  const handleChangeValue = (event) => {
    setValue(event.target.value);
  }

  return children({
    value,
    onChange: handleChangeValue,
  })
}
export default InputHeadless;

그러면 사용처에서는 매개변수로 받는 데이터 로직을 마크업에 따라 마음껏 사용할 수 있다.

  function App() {
  return (
    <div>
      <InputHeadless>
        {({ value, onChange }) => {
          return (
            <div className="input-container">
              <label htmlFor="1">NAME</label>
              <input id="1" type="text" value={value} onChange={onChange} />
            </div>
          )
        }}
      </InputHeadless>
    </div>
  )
}

<참고한 영상>
토스ㅣSLASH 22 - Effective Component 지속 가능한 성장과 컴포넌트
https://www.youtube.com/watchv=aAs36UeLnTg&t=440s

profile
keep on pushing

0개의 댓글