- Published on
재사용 가능한 컴포넌트
- Authors
- Name
- 김희열
재사용 가능한 컴포넌트란 무엇일까요? 너무 당연한 문장이라서 설명이라고 하기에 애매하지만, 키워드 별로 뜯어보면 할 수 있는 말이 많아집니다. 간단하게 우리의 인터넷 선생님 ChatGPT 선생님에게 여쭤보고 시작해볼까요?
이 한 문단에서도 질문할 거리가 참 많습니다. "'독립적인 기능을 가진 모듈'은 무엇일까요?" "다른 컴포넌트와의 결합도는 어떻게 낮출 수 있을까요?", "유지보수와 확장성, 생산성이 높아진다는 것은 어떤 것을 의미할까요?" 그 전에 컴포넌트란 무엇일까요? 혹은 재사용 가능하다는 문장에는 어떤 함의가 있을까요?
컴포넌트와 재사용
컴포넌트
- 컴포넌트란 무엇일까요? 최근 진행하고 있는 과정에서 고민할 거리를 받은 적이 있습니다. "컴포넌트, 리액트 컴포넌트, 웹 컴포넌트 세 가지 관점에서 컴포넌트를 생각해보세요." 라는 문장이었습니다.
- 옥스퍼드 사전에 의하면 컴포넌트란 "A part or element of a larger whole, especially a part of a machine or vehicle."라고 합니다. 말 그대로 더 큰 것의 일부라는 뜻이네요.
- 그렇다면 이 프론트엔드에서 컴포넌트라는 개념이 왜 등장하게 된 것일까요?
컴포넌트 아키텍처
- 컴포넌트의 역사는 웹 개발의 진화 과정과 관련이 있습니다. 초기 웹 개발은 HTML과 CSS를 이용한 정적이고 단순한 문서였지만, 웹 어플리케이션의 등장으로 복잡성이 증가했습니다.
- 사용자 인터렉션과 상호 작용을 통한 즉각적인 UI의 반영이 더욱 중요해졌습니다. 기존 JavaScript의 명령적인 방식으로 문제를 해결하는 것은 이런 복잡성을 해결하는 것에 한계를 드러냈습니다.
- 코드의 구조화, 재사용성이 중요한 이슈로 대두되었고, UI를 작은 부분으로 분리하여 각 부분이 재사용 가능한 독립적인 모듈로 구성되도록 하는 컴포넌트 아키텍처가 대두되었습니다.
- 컴포넌트 기반 아키텍처를 프론트엔드 단에 최초로 적용한 유의미한 프레임워크는 AngularJS였습니다.
- 이후 React, Vue.js 등의 프레임워크, 라이브러리에서 컴포넌트 아키텍처를 사용하면서 폭발적으로 확장하게 되었습니다.
- (개인적 의견) 컴포넌트 아키텍처는 기존 UI 상호 작용의 복잡성을 컴포넌트로 추상화하여 선언적으로 관리한다고 보았습니다. 직접 DOM에 접근해서 문제를 해결했던 기존 방식에서 벗어나 UI 조각들이 '무엇을' 보여줘야 하는지 표현했으니까요.
컴포넌트, 리액트 컴포넌트, 웹 컴포넌트
- 컴포넌트의 사전적 의미는 '구성 요소'입니다. 자동차가 있다면 바퀴는 자동차의 컴포넌트고, 시계가 있다면 시계바늘은 시계의 컴포넌트가 되겠네요.
- 리액트 컴포넌트도 같습니다. 리액트는 UI Library입니다. 데이터와 DOM을 동기화하는 선언적 라이브러리죠. 리액트의 컴포넌트는 상호작용을 동기화하는 UI 조각이라고 할 수 있습니다. 하나의 페이지가 있다고 하면 버튼, 헤더, Input 등이 리액트의 컴포넌트라고 할 수 있습니다.
- 그럼 웹 컴포넌트는 무엇일까요? 리액트 컴포넌트는 웹 컴포넌트와 해결하는 문제가 다르다고 합니다. 리액트 컴포넌트가 선언적으로 데이터와 DOM을 동기화하는 것이라면 웹 컴포넌트는 재사용할 수 있는 컴포넌트에 대한 강한 캡슐화가 목적입니다.
- (고민해 볼 것) 리액트는 어떤 방식으로 데이터와 DOM을 동기화 할까요?
재사용
재사용 가능하다는 것을 조금 더 풀어서 몇 가지 키워드로 설명해볼 수 있습니다.
독립적 모듈
독립적 모듈이란 자체적으로 동작이 가능하며, 다른 컴포넌트에 의존하지 않는 것입니다. 이는 결합도를 낮추고, 컴포넌트의 재사용성을 높일 수 있겠죠.
일관된 인터페이스
독립적으로 동작하지만 인터페이스가 일관되어야 합니다. 그래야 같은 서비스 혹은 같은 프로젝트에서 컴포넌트 사용법이 일관되게 재사용할 수가 있을 것입니다.
유연성 & 확장성
다양한 상황에서 사용할 수 있어야 합니다. 예를 들어 최대 길이를 제한하는 Input 컴포넌트가 있다고 가정해봅시다. 아래와 같이 만들 수 있습니다.
const MaxLengthInput = ({ maxLength }) => {
const [value, setValue] = useState('')
const handleChange = (newValue) => {
if (newValue.length <= maxLength) {
setValue(newValue)
}
}
return <input value={value} onChange={(event) => handleChange(event.target.value)} />
}
그런데 maxLength를 받는 상황은 유연하고 확장성이 있는 걸까요? 물론 재사용할 수 있는 좋은 컴포넌트지만 더욱 확장성있는 Input으로 바꿔볼 수 있을 것 같습니다. 아래와 같이 바꾸면 어떨까요?
const ConditionInput = ({ isConditionMet }) => {
const [value, setValue] = useState('')
const handleChange = (newValue) => {
if (isConditionMet) {
setValue(newValue)
}
}
return <input value={value} onChange={(event) => handleChange(event.target.value)} />
}
이렇게 수정하면 어떨까요? 이제 입력이 가능한 조건은 상위 컴포넌트에서 내려주기 때문에 최대 길이 뿐만 아니라 다양한 조건을 받을 수 있게 되었습니다. 아주 간단한 예시지만 이런 방법론을 가지고 더욱 복잡한 컴포넌트를 유연하게 바꿔볼 수 있습니다.
테스트 가능성
테스트가 용이해야 합니다. 컴포넌트의 동작이 예상대로 이뤄지고, 다른 코드와의 결합도는 최소화하면서, 의도하지 않은 부수 효과없이 동작해야 합니다.
CDD with Storybook
지금까지 재사용 가능한 컴포넌트에 대해서 다뤄보았습니다. 컴포넌트는 프론트엔드 개발 생태계에서 중요한 위치를 가지고 있고, 컴포넌트를 잘 관리한다면 개발 생산성이 높아질 수 있다는 것도 확인했습니다. 그렇다면 어떻게 컴포넌트를 잘 관리할 수 있을까요?
CDD(Component Driven Development)
- CDD 개발 방법론이 그 예시 중 하나입니다. CDD는 컴포넌트 중심의 개발 방법론으로, 도메인 로직을 구현하면서 필요한 컴포넌트를 구현하는 Top-down 방식이 아니라, 도메인과 무관하고 반복적으로 사용할 수 있는 컴포넌트를 먼저 만들고 이를 조합해서 도메인 로직을 완성하는 Bottom-up 방식입니다.
- React로 사고하기 문서를 바탕으로 1단계: UI를 컴포넌트 계층으로 나누기, 2단계: React로 정적인 버전 만들기, 3단계: UI state에 대한 최소한의 (하지만 완전한) 표현 찾아내기, 4단계: State가 어디에 있어야 할 지 찾기 이렇게 4가지 단계로 컴포넌트를 설계할 수 있습니다.
- 이 방법론을 강력하게 도와주는 도구가 바로 아래에서 설명할 Storybook입니다.
Storybook
- Storybook은 UI 컴포넌트나 페이지를 별도로 구축하기 위한 프론트엔드 도구입니다. Storybook을 사용해 UI, 즉 컴포넌트를 따로 빌드하고, 테스트하고, 문서화할 수 있습니다.
- React로 사고하기와 Storybook을 엮어서 사용한다면, 1단계에서 나누고 2단계에서 만든 정적 컴포넌트를 스토리북에 올려서 문서화할 수 있습니다. 그 다음 최소한의 Props와 State로 의존성을 낮춘 독립적인 컴포넌트를 스토리북에서 상호 작용 테스트할 수 있습니다.
- 각종 Addon을 통해 프로퍼티를 실시간으로 조작하여 상호 작용 하는 동안 변경 사항을 확인(Knons)할 수 있고, 컴포넌트에 대한 문서를 작성해서 스토리 파일에 자동으로 포함(Docs)시킬 수도 있습니다. 이 밖에도 접근성, 포맷팅, 액션의 디버깅 & 테스트와 같은 여러 Addon으로 컴포넌트를 관리할 수 있습니다.