2024. 11

자유로운 영혼의 컴포넌트 시스템

보일러플레이트 컴포넌트 시스템

필요성과 개발 배경

어쩌다 첫 번째 개발 커리어가 회사가 아니라 프리랜스 개발이 되었는데요, 아무튼 개발로 돈을 벌 기회를 가지게 되었다는 건 좋은 경험이었습니다. 그래서 이 글을 왜 쓰냐면, 아무래도 프리랜서 개발자의 길을 걷게 될…것 같다는 건 진짜 농담이고, (저는 고정수입과 루틴과 출퇴근이 있는 생활을 정말 원합니다) 다음에 사이드 프로젝트나 프리랜스 개발을 할 때를 대비해 빠르게 작업에 돌입할 수 있는 템플릿을 만들어두는 작업을 해 보려고 합니다.

최근 여러 개의 프로젝트를 연달아 시작하게 되면서 정말 성가신 작업을 했고 atomic한 컴포넌트 부분을 복사해서 붙여넣기하는 작업을 하게 됐는데요, 이 부분을 npm 패키지로 배포해서 해결할 수 있으면 참 편할 것 같다는 생각을 하게 됐습니다. 이번에 프리랜스 개발을 하면서 제가 작성해 두었던 오픈소스의 덕을 톡톡히 본 것이 있어서, 컴포넌트도 이렇게 만들어 두고 싶었습니다.

보일러플레이트 컴포넌트

https://github.com/vvyre/component

https://www.npmjs.com/package/@syyu/component

npm install @syyu/components

yarn add @syyu/components

제가 생각하는 보일러플레이트 컴포넌트는 atomic 컴포넌트입니다. 즉, 요구사항 중 가장 작은 단위의 컴포넌트를 말합니다. 따라서 마크업 단위의 컴포넌트라고 할 수 있는데요, 구현 대상으로 축약해보면 아래와 같습니다.

  • p

  • span

  • button

  • div

  • li, ul, ol,

타입과 마크업

컴포넌트 Props의 타입 정의를 프로젝트를 시작할 때마다 작성해야 한다는 건 여간 귀찮은 일이 아닙니다. 다만 꼭 필요한 일임에는 자명합니다. 어떤 요구사항이 들어올지 모르고 가능한 한 다양한 동작을 감당할 수 있어야 하는 만큼 최대한 유연해야 한다고 생각했습니다. 따라서 일부 컴포넌트는 다형 컴포넌트의 형태를 취합니다. 아래는 View 컴포넌트의 코드입니다. 기본적으로는 <div> 마크업으로 렌더링되지만, as 프롭을 통해 다른 형태로도 렌더할 수 있습니다.

Typescript
type ComponentProps<T extends ElementType> = {
	as?: T;
	...
} & ComponentPropsWithRef<T>

function Component<T extends ElementType>
	({ as, ...props }: ComponentProps<T>) {
		const Comp = as || 'div'
		return <Comp {...props}>{props.children}</Comp>
}

export const View = forwardRef(Component)

API

위의 인터페이스를 기반으로 제작한 컴포넌트들은 다음과 같습니다. (* 표시는 다형 컴포넌트입니다)

  • Button* (<button>)

  • Flex*

  • Form (<form>) / Binded Form (form, input, label, button*)

  • Input (<input>) / TextArea(<textarea>)

  • Label (<label>)

  • List* (<ul>, <ol>, <li>, ...)

  • Spacing (<div>, ...)

  • Txt* (<hn>, <span>, <p>, ...)

  • View* (<div>, ...)

주목할 만한 컴포넌트는 Binded Form이 있습니다. 이전에 만들어 배포했던 useForm Hook을 조금 더 편하게 사용할 수 있도록 컴포넌트화했습니다. 안에는 Label, Input, TextArea, Button이 포함되어 있고요, 아래와 같이 간단하게 사용할 수 있습니다. (context도 함께 제공하긴 하지만 권장하지는 않습니다)

Typescript
const handleSubmit = ({ id, password }) => {
  //fetch or do sth
}

//필수 프랍은 폼 초기값과 onFormSubmit
//form name과 초기값의 key로 매치하면 바인드됩니다
	return (
		<Bc.BindedForm
			initialValues={{ id: '', password: '' }}
			onFormSubmit={handleSubmit}>
		  <Bc.BindedForm.Input name='id' />
		  <Bc.BindedForm.Input name='password' />
		  <Bc.BindedForm.Button> 로그인! </Bc.BindedForm.Button>
		</Bc.BindedForm>)
}

복잡한 상황에서는 useForm hook을 사용해야 하지만, 간단한 동작만 구현할 거라면 BindedForm이 큰 역할을 할 수 있습니다. useForm이 지원하는 범위 안에서 사용할 수 있습니다 (input type number, type checkbox, textarea, …)

기본 스타일링 라이브러리: Emotion

스타일링은 글로벌 레벨에서 기본 스타일을 삭제한다고 가정하고 아무런 스타일링을 하지 않은 상태에서, 상위에서 오버라이드하는 방식을 가정했습니다. 즉 조립하는 컴포넌트 또는 동일 선상에서 모든 스타일을 재작성하는 방식을 채택했습니다.

또 사용하게 될 스타일링 라이브러리에 대한 고민이 있었습니다. 후보로 생각한 건 Vanilla-ExtractEmotion인데요, 제가 최근 사용했던 Vanilla-Extract의 (Emotion 대비) 장단점은 다음과 같습니다.

장점

  • Zero-Runtime CSS 라이브러리

  • 기본적으로 인라인 스타일로 작동하지 않으며 빌드 시 순수한 css 파일을 결과로 내놓음

    • 초기 렌더링 시 TTV 감소

    • 번들 크기 감소

  • CSS-IN-TS라고 주장할 만큼 타입 안정성이 비교적 더 보장됨

단점

  • SSG/ISR로 생성한 페이지 간에 라우트를 이동할 때 스타일이 캐싱되지 않은 경우 CSS 번들이 따로 로드되므로 화면 깜빡임이 있음 (FOUC, Flash of Unstyled Content)

    • 다만 Emotion을 사용하더라도 이 현상이 근본적으로 해결되는 것은 아님 (SSR 시)

  • 자료 및 커뮤니티가 emotion에 비해 크지 않음

  • 동적 스타일링이 까다로움

저는 이 컴포넌트 시스템으로 복잡한 협업 프로젝트가 아닌 작은 프리랜스 프로젝트들을 하게 될 예정이라 Emotion으로 결정하게 됐습니다. 구체적인 이유는 아래와 같습니다.

  1. 스타일링 코드를 컴포넌트에서 분리하도록 강제할 필요가 없으며, (필요하다면 분리가 가능합니다)

  2. 어떤 프로젝트에서도 대응 가능한 유연한 동적 스타일링이 필요하고,

  3. Emotion의 스타일 오버라이드가 가지는 이점이 더 크다고 판단했습니다. (Vanilla-Extract의 경우 전체 스타일 프로퍼티가 덧씌워집니다)

테스트 및 배포

BindedForm만 테스트를 작성했습니다(jest). 또, SSR 환경에서도 바로 사용할 수 있도록 하기 위해 dual cjs/esm 패키지로 배포했습니다. 저번 오픈소스에는 Bunchee라는 간단한 툴(이것도 Rollup 기반이긴 합니다)을 사용해 빌드했었는데, 이번에는 Rollup을 직접 설정해 사용해 보았습니다. 이것저것 설정하느라 고생을 좀 하긴 했는데, 어쨌든 오픈소스를 빌드해 배포하는 건 한글로 된 정확한 정보가 없어서 조금 오래 걸렸네요.

후기

사용해 본 후기는 만족스럽습니다. 타입도 잘 잡아주고, 패키지 하나로 바로 구현에 들어갈 수 있어서 작은 프로젝트를 이것저것 만들기를 좋아하는 저로서는 참 좋은 것 같습니다. 관심이 있으신 분들은 아래 링크에서 확인해 보시거나 명령어로 설치해 사용해보실 수 있습니다. 최근에는 React Native를 공부하고 있는데 이쪽으로도 확대해 보려고 합니다. 피드백이나 조언이 있으시다면 댓글을 달아 주세요. 감사합니다.

https://github.com/vvyre/component

https://www.npmjs.com/package/@syyu/component

npm install @syyu/components

yarn add @syyu/components

Example)

Javascript
import { Bc } from ‘@syyu/components’

...

	return <Bc.View as="span"> ... </Bc.View>
}