Headless 패턴

Headless 개념은 소프트웨어 개발에서 주로 사용되는 용어로, 사용자 인터페이스(UI)를 가지지 않은 컴포넌트 또는 서비스를 의미합니다. 일반적으로 웹 애플리케이션 또는 CMS(Content Management System)과 같은 시스템에서 사용됩니다.

Headless 개념은 UI를 분리하여 개발하는 아키텍처 패턴을 지칭합니다. 기존의 전통적인 웹 애플리케이션 개발에서는 백엔드와 프론트엔드가 긴밀하게 결합되어 있었습니다. 하지만 Headless 개념을 도입하면 백엔드와 프론트엔드를 독립적으로 개발하고, 통신하는 API를 통해 데이터를 주고받을 수 있습니다.

Headless 개념에서는 백엔드가 데이터와 비즈니스 로직을 처리하고, 프론트엔드는 이러한 데이터를 가져와 UI를 구성하는 역할을 합니다. 이렇게 분리된 아키텍처를 통해 다양한 프론트엔드 플랫폼(웹, 모바일 앱, IoT 등)에서 동일한 백엔드 서비스를 사용할 수 있으며, 컴포넌트 기반의 재사용성과 유연성을 높일 수 있습니다.

Headless 아키텍처의 주요 이점은 다음과 같습니다:

유연성: 데이터와 비즈니스 로직은 백엔드에서 처리되므로 프론트엔드는 필요에 따라 다양한 방식으로 데이터를 표현하고 사용자 경험을 개선할 수 있습니다.

재사용성: 백엔드 API는 다양한 프론트엔드 플랫폼에서 재사용할 수 있으며, UI 컴포넌트도 다른 애플리케이션에서 재사용할 수 있습니다.

확장성: 백엔드와 프론트엔드가 독립적으로 확장 가능하므로, 시스템이 성장하거나 변화하는 요구사항에 대응하기 쉬워집니다.

개발 팀 간 협업: 백엔드와 프론트엔드 개발 팀이 독립적으로 작업할 수 있으므로 개발 프로세스가 병렬로 진행될 수 있고, 팀 간 협업이 용이해집니다.

1. Compound(화합물) 컴포넌트

Compound Component는 리액트(React)에서 사용되는 패턴 중 하나입니다. 이 패턴은 부모 컴포넌트와 그 자식 컴포넌트들로 구성된 복합적인 컴포넌트를 생성하는 방법을 제공합니다.

일반적으로 리액트에서 컴포넌트 간에 데이터를 전달하기 위해서는 props를 사용합니다. 그러나 Compound Component 패턴을 사용하면, 부모 컴포넌트가 자식 컴포넌트들에게 직접적으로 데이터를 전달할 수 있습니다. 이는 부모 컴포넌트와 자식 컴포넌트들 사이의 관계를 더욱 직관적으로 만들어줍니다.

Compound Component 패턴은 주로 UI 컴포넌트 라이브러리에서 사용됩니다. 예를 들어, 탭(Tab) 컴포넌트를 생각해보겠습니다. 탭 컴포넌트는 여러 개의 탭을 가지고 있고, 각 탭을 클릭하면 해당 탭의 내용이 보여집니다. 이때, Compound Component 패턴을 사용하면 부모 컴포넌트는 탭 컴포넌트 자체를 생성하고, 자식 컴포넌트로 각 탭을 구성하는 탭 아이템(TabItem) 컴포넌트를 전달할 수 있습니다.

부모 컴포넌트는 탭 컴포넌트의 속성을 통해 선택된 탭을 추적하고, 자식 컴포넌트에게 필요한 데이터를 전달할 수 있습니다. 자식 컴포넌트들은 부모 컴포넌트에서 전달받은 데이터를 사용하여 각 탭 아이템을 렌더링하고, 이벤트 핸들러를 등록할 수 있습니다.

이렇게 Compound Component 패턴을 사용하면, 부모 컴포넌트와 자식 컴포넌트들 사이의 관계가 더욱 명확해지고, 컴포넌트의 사용자가 더 쉽게 컴포넌트를 조작하고 커스터마이징할 수 있습니다.

compoundInput.tsx

import React, { useContext } from "react";

/** context api를 이용해서 컴포넌트 내부에서 공유할 데이터를 정의함 */
const InputContext = React.createContext({
  id: "",
  value: "",
  type: "text",
  onchange: () => {},
});

/** 부모 컴포넌트, context api를 통해 데이터를 공유할 수 있도록 설정 */
export const InputWrapper = ({ id, value, type, onChange, children }) => {
  const contextValue = { id, value, type, onChange };
  return (
    <InputContext.Provider value={contextValue}>
      {children}
    </InputContext.Provider>
  );
};

/** 자식 컴포넌트, context api를 통해 데이터 사용함
	부모 컴포넌트가 props로 받아서 context에 저장시킨 기본 데이터도 가져오고, 추가로 자신만의 props를 받아서 사용할 수도 있음
*/
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 id={id} {...props}>
      {children}
    </label>
  );
};

/** 자식 컴포넌트를 부모 컴포넌트의 props로 등록함 */
InputWrapper.Input = Input;
InputWrapper.Label = Label;

App.js

/** 데이터 관리 */
  const [name, setName] = useState("");

  const handleChange = (event) => {
    setName(event.target.value);
  };

<InputWrapper id="name" value={name} type="text" onChange={handleChange}>
        <InputWrapper.Label>Name</InputWrapper.Label>
        <InputWrapper.Input />
</InputWrapper>

2. Function as Child 컴포넌트

자식 요소에 어떤 것이 들어올지 모르는 상황에서, 부모 요소는 오직 데이터 로직만 갖고, 자식 요소를 컴포넌트 통째로 받도록 구성하는 컴포넌트입니다.

실제 컴포넌트 사용처인 App.js에서는 ui와 관련된 마크업을 원하는대로 마음대로 바꾸어서 쓰고, 데이터 로직은 컴포넌트 내부에서 설정 및 관리하는 것입니다.

functionAsChildInput.js

import React, { useState } from "react";

const FunctionAsChildInput = ({ children }) => {
  const [value, setValue] = useState("");

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

  return children({
    value,
    onchange: { handleChange },
  });
};

export default FunctionAsChildInput;

App.js

<FunctionAsChildInput>
        {({ value, onChange }) => {
          return (
            <div className="input-container">
              <label id="1">Name</label>
              <input type={"text"} id="1" value={value} onChange={onChange} />
            </div>
          );
        }}
</FunctionAsChildInput>

3. Custom Hook 패턴

useInput이라는 훅을 만들어서, 데이터 로직을 다 구현해놓고 실제 사용처에서는 비구조화 할당을 통해 훅을 통해 반환된 객체의 속성을 추출해서 각각의 변수에 할당해서 사용하려는 코드입니다.

아래 코드의 경우, value 속성은 name 변수에 할당되고 onChange 속성은 onChangeName 변수에 할당되었습니다.

useInput.js

function useInput() {
  const [value, setValue] = useState('');

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

  return { value, onChange };
}

App.js

const {value : name, onChange : onChangeName} = useInput()

<label htmlFor='1'>Name</label>
<input type="text" id='1' value={name} onChange={onChangeName} />

참조링크

[10분 테코톡] 호프의 프론트엔드에서 컴포넌트

profile
Front-end | Web Develop | Computer Science 🧑🏻‍💻

0개의 댓글