Vibrant Design System 과 함께하는 크로스 플랫폼 개발기

Vibrant Design System Episode 2

Peter
CLASS101
11 min readMar 15, 2023

--

Vibrant Design System 두 번째 스토리입니다.
이 글은 Vibrant Design System을 크로스 플랫폼으로 개발하는 과정을 보여드리기 위해 작성되었습니다.

기존 클래스101 서비스는 웹과 앱이 각자 다른 코드로 개발되어 있었습니다.
하지만 Vibrant Design System이 도입된 뒤에는 플랫폼을 구분 짓지 않고 UI를 개발하고 있습니다.

우리가 이러한 Cross-Platform UI 라이브러리를 제작하기까지의 일들을 설명하기 위해 과거에 클래스101에서 사용 중이었던 class101-ui 라이브러리 이야기부터 시작해 보려고 합니다.

CLASS101 UI

대부분의 디자인 시스템 컴포넌트들이 비슷하지만, 디자이너와 개발자와 소통할 때 색은 주로 토큰으로, 사용되는 UI들은 컴포넌트 이름과 속성으로 소통하고 있습니다. 이는 과거 class101-ui 라이브러리도 동일했습니다.

만약 디자인에 ‘바로 시작하기’라는 버튼이 있으면 프로덕트 디자이너는 개발자에게 ‘이 위치에는 small size primary Button이 필요하다’라는 이야기를 합니다. 그럼 웹 개발자는

“이 위치에는 Button 컴포넌트가 들어가고, 해당 버튼의 kind는 primary, size는 small이다”라고 생각한 뒤 바로 시작하기 버튼을 저 위치에 배치합니다. 코드로 보면 전달받은 내용 거의 그대로 코드에 반영하여 개발의 난이도가 쉬웠습니다.

import { Button } from "@class101/ui";<Button kind="primary" size="small">
바로 시작하기
</Button>

이렇게 웹 개발자는 class101-ui 라이브러리를 통해 개발했습니다

다만 여기서 앱 개발자는 동일한 방식으로 코드를 작성할 수 없었습니다. 앱은 당시 네이티브 디자인 시스템 라이브러리가 존재하지 않아, 앱 개발자는 ‘바로 시작하기’ 버튼을 개발하기 위해서 하나씩 스타일을 분해해가면서 코드를 짜야 했습니다. 저 위치에 Primary 버튼이 존재하니까 이 버튼의 패딩은 8이고, 높이는 40 px이고, 텍스트 폰트 사이즈는 14이고… 이런 식으로 하나씩 손수 개발해나가면서 네이티브 앱을 개발하고 있었습니다.

const Button = styled.TouchableOpacity`
height: 40px;
background-color: $(primary};
align-items: center;
justify-content: center;
padding: 8px 7px;
`;
const ButtonText = styled.Text'
${body2Medium};
color: white;
';
<Button>
<ButtonText>바로 시작하기</ButtonText>
</Button>

그러다 보니 일반적인 웹 개발과 비교했을 때 네이티브 개발 쪽 코드의 양이 보통 3배에서 5배 정도 많았습니다.

“네이티브에서도 class101-ui 라이브러리에 있는 컴포넌트를 쓰면 되지 않겠냐”라고 생각하실 수 있지만, 이는 금방 적용하기가 쉽지 않았습니다.

현재 클래스101은 웹과 앱이 같은 언어로 개발 중이지만, 네이티브는 React Native로 일반 웹 개발과는 약간 다른 방식으로 개발이 되어야만 했습니다.

더 상세한 예를 들어 웹 개발자는 <div style="width: 100%"/> 와 같이 HTML의 Element을 이용하여 앱을 개발해야 하지만 네이티브 개발자는 약간 다른 개념으로 이 위치에 <View style={{ width: '100%' }} />와 같이 React Native에서 제공하는 View라는 컴포넌트로 코드를 작성해야 합니다. 하지만 웹 개발만을 중점으로 기존 class101-ui이 개발이 되었기 때문에 HTML Element를 사용한 코드는 네이티브에서 실행될 수 없었습니다

이렇게 class101-ui 기반으로 개발을 해나가는 와중 네이티브 디자인 시스템이 절실히 필요해져서 그 당시에 새로 개발되고 있는 디자인 시스템에 포함시키기로 했습니다. 해당 디자인시스템의 초기 이름은 One Product System 즉 OPS라고 불렀습니다. (One Product System 개발기 보러가기)

CLASS101 One Product System

해당 디자인 시스템을 초반에 구축할 때는 Web과 Native 컴포넌트를 따로 구현하여 같은 api만 제공하고, 웹이랑 네이티브를 디자인 시스템 내부에서 따로 개발하는 방식이었습니다. 그렇게 하면 아래처럼 ContainedButton을 두 번 개발하면 되고, TextField를 두 번 개발하면 되고.. 이런 식으로 각자 개발해나가는 것을 생각했었습니다.

하지만 좀 더 개발에 대해 이야기하고 맞춰 나가다 보니 각 컴포넌트들에 작성해야 하는 코드가 많아지게 되고, 결국 Box라는 컴포넌트를 먼저 만들기로 했습니다.

해당 Box를 기반으로 ContainedButton, TextField, Switch, Badge 등 여러 컴포넌트들이 만들어지도록 한 뒤, 네이티브와 웹을 동시에 지원하기 위해 Box가 웹, 네이티브 둘 다 지원해야 했습니다.

그래서 Box 컴포넌트를 개발할 때 모든 스타일을 다 받을 수 있게 한 웹용 박스(Box.tsx)와 네이티브용 박스(Box.native.tsx)를 만들어서 ContainedButton 등 여러 컴포넌트를 만들 때 Box 컴포넌트를 통해 만들도록 하여 자연스럽게 ContainedButton도 크로스 플랫폼을 지원하게 만들었습니다.
(React Native에서는 Box.tsx를 불러오면 metro가 자동으로 Box.native.tsx를 찾습니다)

하지만 우리 웹사이트 상에 표시되는 모든 ui가 하나의 Element 일 수는 없습니다. 예를 들어 모든 Element가 div로 이루어져 있으면 우리 비디오를 웹사이트에 띄울 수 없고, 버튼도 띄울 수 없고, 이미지도 띄울 수 없습니다.
결국 이 박스 하나만으로는 모든 ui를 표현할 수 없기 때문에 우리는 박스에 as라는 속성을 추가했습니다.

이 as에는 HTML5 태그가 들어가게 되어서 p, img와 같은 여러 태그들을 박스에 넣어서 그 박스가 이루어진 element를 바꿀 수 있게 했습니다.

as를 사용해서 ContainedButton을 간단하게 구현해 보면 다음과 같은 코드가 나옵니다

ContainedButton은 버튼이기 때문에 박스에 as=”button”이 들어가게 됩니다. 이 박스 버튼 안에 텍스트가 들어가는데 텍스트는 as=”span”으로 텍스트를 넣을 수 있고, rightIcon이 존재하면 rightIcon을 감싸주는 마진을 주기 위해서 박스를 하나 주게 됩니다. 이렇게 하면 이 컴포넌트에는 세 가지의 박스가 존재합니다

  1. button으로 동작되는 박스
  2. text가 들어가는 박스
  3. 그냥 박스

이렇게 세 개로 이루어지게 됩니다.

이 컴포넌트가 웹에서 실제로 만들어지는 HTML을 보면 button, span, div으로 동작됩니다.

다만 앱(React Native)은 HTML로 동작되는 게 아니니 리액트 네이티브에서 존재하는 Button, Text, View 컴포넌트들로 동작됩니다

이렇게 하면 모든 UI 컴포넌트들을 개발할 때 크로스 플랫폼을 따로 생각하지 않으면서도 빠르게 개발할 수 있겠다고 판단했습니다.

이제 Box의 상세한 구현을 보면 박스에 as=”p”를 추가하게 될 때 웹에서는 <p></p>로 동작됩니다. 네이티브에서는 p 태그가 존재하지 않으니까 as=”p”를 보고 Text로 변환하게 됩니다. as=”p”는 결국 웹의 api를 참고해서 만든 것이 되어버려서 Web First API가 되었습니다

여기서 네이티브 박스의 구현을 살펴보면 박스를 구현할 때 as=”p”이면 Text로 동작이 되고, 그렇지 않으면 View로 동작되도록 할 수 있습니다.

이렇게 구현하니 div, h1, img, 와 같은 태그들은 하나씩 매핑할 수 있었습니다. 하지만 as 속성만으로는 해결할 수 없는 문제가 있었는데, 바로 스크롤이었습니다.

이제 웹에서는 css로 div가 존재하고, 이 div가 overflow 됐을 때 scroll을 쓰게 한다는 걸 css에 넣어주면 되는데, 앱에는 overflow: scroll 속성이 없습니다. 하지만 리액트 네이티브에서 ScrollView에서 scroll을 지원하기 때문에 대신 ScrollView를 사용해야 합니다.

네이티브 박스 구현을 다시 보면 이제 if문이 하나 추가가 됐습니다.

이미 벌써 코드의 느낌이 좋진 않지만.. 다음 사례를 살펴볼까요?

이제 Switch 컴포넌트가 나왔습니다!

이 Switch 컴포넌트 누르면 양옆으로 이동하는 체크 박스와 같은 동작을 하는 컴포넌트입니다. 예시 코드에서는 Switch 동작을 위한 많은 스타일들이 생략되어 있긴 하지만, Switch에는 많은 스타일이 들어가겠죠?

그럼 웹에서는 이 컴포넌트는 input element로 이루어졌고, type은 “checkbox”이고, css는 그대로 들어가면 되지만, 리액트 네이티브에서는 input이라는 컴포넌트가 없고, 체크 박스를 구현하기 위해서는 완전히 다르게 구현되어야 합니다.

마침 React Native는 Switch라는 컴포넌트를 따로 제공하는 것 같으니, 적절한 if문으로 해결할 수 있을까요?

첫 Box 구현에 비해 if문이 엄청나게 복잡해졌습니다.

이렇게 된 배경은 Box의 API를 웹(HTML) 기준으로 설계하게 되면서 네이티브 대응을 위한 코드들이 엄청나게 많아져 결국 Box 컴포넌트부터 시작하여 API를 다시 정의해야 하는 필요성에 직면했고, 우리는 새로운 디자인 시스템을 만들게 됩니다.

CLASS101 Vibrant Core

Vibrant Design System을 개발하게 되었을 때는 기존 Box 컴포넌트의 역할을 줄이고, 기존에 Box가 담당하는 역할들을 분리하기로 했습니다. 예를 들어 Box를 Text, ScrollBox, PressableBox, Transition(animation)을 담당하는 박스들로 만들었습니다. 각각의 역할에 맞게 코어 컴포넌트들을 분리하기 시작했습니다.

이렇게 이렇게 되면 ContainedButton을 아래와 같이 구현할 수 있습니다.

이제 각 컴포넌트들이 담당하는 역할들이 깔끔해 보이고, 더 이상 Box 컴포넌트 내에서의 분기가 필요해지지 않게 되었습니다.

이제 다시 ScrollBox 컴포넌트를 살펴볼까요?

ScrollBox 컴포넌트는 이제 스크롤할 수 있는 자신의 역할만 충실해질 수 있습니다. 해당 컴포넌트는 ScrollBox.tsx, ScrollBox.native.tsx 두개로 나뉘어서 플랫폼별로 각각 개발이 이루어지게 되어 이전의 Box 컴포넌트처럼 로직이 무거워지는 것을 막을 수 있었습니다.

현재 Vibrant Core 컴포넌트들을 이렇게 구성하여 많은 프로덕트 컴포넌트들을 크로스 플랫폼이 지원되도록 개발하고 있습니다

Vibrant Design System 크로스 플랫폼 개발기는 여기까지 하겠습니다.

Vibrant Design System Stories

ep.#1 Vibrant Design System을 소개합니다.

ep.#2 Vibrant Design System과 함께하는 크로스 플랫폼 개발기

클래스101 테크팀이 더 궁금해졌고 성장하는 서비스와 조직에 기여하고 싶은 분이라면, 아래 링크를 확인해 주세요.😉

--

--