[인턴일지] MISE - 2 (디자인시스템)

Sio·2022년 3월 1일
2

디자인시스템 (feat.토스)

혹시 토스(Toss)의 디자인 시스템(Desing System), 이하 TDS를 들어보셨나요?

혹시 처음 들으신 분들은 아래 링크를 참고하면 좋을것같아요✨

참고 블로그
https://blog.toss.im/tag/%ED%86%A0%EC%8A%A4-%EB%94%94%EC%9E%90%EC%9D%B8-%EC%8B%9C%EC%8A%A4%ED%85%9C

참고 노션
https://www.notion.so/1000-755c0950270c4f779f1979c2c3258ea6

실제로 TDS의 존재로 30-40분 걸릴 화면 디자인이 3-4분에 끝나며, 코드 길이는 50%로 줄었다는 것을 생각하면, 매번 UI 개발 작업을 해야하는 프론트엔드 개발자에게 참 고마운 시스템이 아닐 수 없을것입니다🤩

마이스에서는 매주 화요일마다 개발팀, 데이터팀, 기획팀, 마케팅팀, 디자인팀이 모두 모여 각자의 진행상황과 이슈, 목표등을 공유하는 스크럼을 가졌는데요.

이 때 개발팀에서 제안한 것이 리액트 쿼리 기술과 체리 앱만의 고유한 디자인 시스템 이랍니다.
(리액트 쿼리 이야기는 뒷편에서 해볼게요👍🏻)

체리 앱에서도 분명히 반복적으로 쓰이는 배경, 헤더(header), 푸터(footer), 버튼등이 존재 하였는데 몇 가지를 제외하고는 매번 같은 코드를 반복해서 작성하고 있었습니다. 그래서 XD, 제플린 등을 이용하여 스타일 가이드를 작성하고 이것들을 코드로 옮기는 작업을 시작하였습니다!

color

먼저 제플린 스타일가이드에 아래와 같이 색이 정의되어 있습니다.

이것들을 코드로 옮기면 어떻게 될까요?
object 형태로 정리해봅시다.

const theme = {
  Blue1: '#F5F7FF',
  Blue2: '#E6EBFF',
  Blue3: '#aab9ff',
  SubBlue: '#6F87FF',
  CheryBlue: '#5471FF',
  Red: '#F25555',
  Yellow: '#FFD833',
  White: '#FFFFFF',
  LightGray1: '#F5F5F5',
  LightGray2: '#DBDCDC',
  Gray1: '#B5B7BA',
  Gray2: '#909398',
  DarkGray1: '#5C5F66',
  DarkGray2: '#292C34',
};

export default theme;

실제 css적용은 다음과 같이 하면 됩니다.

import theme from '../../style/theme';

return <Text 
	color={theme.CheryBlue}
>

매번 16진수의 컬러 코드를 입력하는 것보다 훨씬 가독성 있고 시간이 절약될 거에요:)

Text

color만큼 자주 쓰이는 Text 요소도 한번 코드화 시켜볼까요?
제플린 텍스트 스타일에 디자이너님이 정리를 해주셨네요😍

체리 프로젝트에서는 GlobalStyled라는 파일 내에서 배경 및 텍스트를 정의해놓았어요!

const GlobalStyled = {
  Noto1: styled.Text`
    font-size: 24px;
    font-weight: ${(props) =>
      props.fontWeight ? fontWeight[props.fontWeight] : '700'};
    font-family: ${(props) =>
      props.fontFamily ? fontFamily[props.fontWeight] : 'NotoSansKR-Bold'};
    include-font-padding: false;
    color: ${(props) => (props.color ? props.color : theme.DarkGray2)};
  `,
	Noto2: ...,
    Noto3: ...,
    Noto4: ...,
    Noto5: ...,
    Noto6: ...,

export default GlobalStyled;

실제 적용은 이렇게 하면 됩니다.
폰트색, 굵기 등은 prop으로 간단하게 넘겨주고, margin같은 다른 요소들은 style 프롭으로 넘겨주면 되겠죠?

import GlobalStyled from '../../style/GlobalStyled';
import theme from '../../style/theme';

return <GlobalStyled.Noto1
          color={theme.DarkGray2}
          fontWeight={'Regular'}
          style={{ marginTop: 15 }}
        >
          {title}
        </GlobalStyled.Noto1>

Background

체리 앱의 모든 페이지에 쓰인다고해도 과언이 아닌 배경 부분도 살펴볼게요.
역시 GlobalStyled에 정의되어 있는데요,

const GlobalStyled = {
  ViewCol: styled.View`
    display: flex;
    flex-direction: column;
    width: ${(props) => (props.width ? parser(props.width) : '100%')};
    height: ${(props) => (props.height ? parser(props.height) : '100%')};
    justify-content: ${(props) =>
      props.justifyContent ? props.justifyContent : 'center'};
    align-items: ${(props) => (props.alignItems ? props.alignItems : 'center')};
  `,
  ViewRow: styled.View`
    display: flex;
    flex-direction: row;
    width: ${(props) => (props.width ? parser(props.width) : '100%')};
    height: ${(props) => (props.height ? parser(props.height) : '100%')};
    justify-content: ${(props) =>
      props.justifyContent ? props.justifyContent : 'center'};
    align-items: ${(props) => (props.alignItems ? props.alignItems : 'center')};
  `,
  WhiteSpace: styled.View`
    height: ${(props) => (props.height ? parser(props.height) : '10px')};
  `,
  Noto1: ...,
  .
  .
  .
};

export default GlobalStyled;

prop으로 넘어오는 값들을 css 문법에 맞게 바꿔주는 코드인 parser은 다음과 같습니다.

const parser = (value) => {
  if (typeof value === 'string') {
    if (value.substring(value.length - 2, value.length) == 'px') {
      return value;
    } else if (value.substring(value.length - 1, value.length) === '%') {
      return value;
    } else {
      return value + 'px';
    }
  } else {
    return value + 'px';
  }
};

ViewCol은 말 그대로 flex-direction이 column인 것 -> 위에서 아래로 수직으로 쌓이는 뷰입니다.
ViewRow는 flex-directiondl row인 것 -> 왼쪽에서 오른쪽으로 수평으로 쌓이는 뷰입니다.
WhiteSpace는 컴포넌트를 배치할 때 사이에 빈 공간을 자동으로 채워주기 위한 장치랍니다. 크기를 지정해준다면 margin과 비슷한 역할을 할 수도 있고, 아니라면 남은 공간을 자동으로 차지 하므로 반응형 앱을 만드는데 적합하겠지요?✨

이 프로젝트에서만 정의되는게 아니라, 리액트 네이티브 개발을 하는데 있어서 편리하게 쓰이수 있는 스타일들이라고 생각해요!

삼항연산자

삼항연산자는 간단히 보도록할게요 (리액트 네이티브 개발을 하면서 필수로 알아야하는 js문법 중 하나라고 생각해요. 다른 포스팅에서 자세히 정리해보겠습니다.)

이 부분에 쓰인게 삼항연산자 문법입니다.

width: ${(props) => (props.width ? parser(props.width) : '100%')};

if문이라면 어떻게 될까요?

width: ${(props) => {
	if(props.width) {
		return parser(props.width)
	}
	else {
		return '100%'
	}
  }
};

삼항연산자를 쓰는게 훨씬 길이도 짧고 편리합니다!

WhiteSpace는 어떻게 쓰일까요?

제가 만든 체리의 비밀번호 변경페이지 입니다.
아래 빨간 네모박스가 WhiteSpace랍니다.

코드로 자세히 살펴보면

return (
	<GlobalStyled.ViewCol 
    	style={{
        	paddingHorizontal: 16,
    	}}
    >
    ...
    	<GlobalStyled.Noto6 fontWeight={'Regular'}>
        	비밀번호는 8-16자리의 영문/숫자 조합으로 입력해주세요.
        </GlobalStyled.Noto6>
        
        <WhiteSpace style={{ flex: 1 }}/>
        
        <GlobalStyled.ViewRow style={{ height: 'auto'}}>
        	<Button />
            <Button />
        </GlobalStyled.ViewRow>
    
   
    </GlobalStyled.ViewCol>
)

'새 비밀번호를 입력해 주세요!' 타이틀부터 시작해서 footer 부분의 버튼까지 위에서 아래로 쌓이므로 flex-direction이 column인 컨테이너 ViewCol을 써주는게 맞습니다.

쭉 내려오다, '비밀번호는 8-16자리 ~ 입력해주세요' Text 요소와 하단 버튼 사이에 빈 공간이 있죠? 물론 높이를 지정해주어도, marginTop이나 End를 주어서 해결할 수도 있지만, flex:1 짜리 뷰 하나를 배치하면 어떻게 될까요?
해당 뷰가 두 요소 사이를 자동으로 채워버리겠죠?

즉, 고정값을 알지 않아도 디바이스마다 자동으로 채워지는 반응형을 구현할 수 있습니다!

widthPercentageToDP / heightPercentageToDp

import { 
		widthPercentageToDP as wp,
		heightPercentageToDp as hp,
} from 'react-native-responsive-screen';

wp와 hp를 들어 보셨나요? (저도 모르게 위피랑 히피로 불렀던것같아요😆 이유는 모릅니다...)

아래는 npm 공식 문서입니다.

https://www.npmjs.com/package/react-native-responsive-screen

어떤 npm 라이브러리를 택해야 할지 고민될땐?
💡 저는 사이트 우측의 다운로드 수나 마지막 업데이트 일, 깃헙 PR수 등을 보고 결정하곤해요.
많은 사람이 쓰는 것이 꼭 좋은 라이브러리라는 보장은 없지만, 그만큼 사용자가 누적되어 잘 사용하고 있다는 뜻으로 생각해서 참고하여 사용중입니다!

먼저 npm install을 해주고 import 해주면 사용할 준비는 마쳤습니다.

npm install react-native-responsive-screen --save

엄청 길게 사용법이 적혀잇는데 결론은 기기의 스크린 높이와 너비를 자동으로 감지하여,
'%값을 넘겨주면 dp(px)값으로 반환해준다' 입니다.

예를들어, 삼성 A5모델의 스크린 너비는 360dp 인데, width: wp('50%') 또는 wp(50)을 주게되면 180dp가 되는것입니다.

그러면 width: 50% 와 width: wp(50)은 뭐가다르죠?
싶을 수도 있습니다!

먼저 전자는, 부모 뷰 기준으로 %를 재는 방식입니다.
예를 들어 봅시다.

<GlobalStyled.ViewCol style={{ flex: 1 }}>
	<Globastyled.ViewCol 
    	style={{ 
        	flex: 1, 
            backgroundColor: 'tomato'}} >
    </GlobalStyled.ViewCol>
    
	<Globastyled.ViewCol 
    	style={{ 
        	flex: 1, 
            backgroundColor: 'yellow'}} >
    </GlobalStyled.ViewCol>
</GlobalStyled.ViewCol>

위와 같은 코드와 화면이 있다고 해봅시다. 전체 column 뷰 안에 flex:1로 똑같이 나눠진 두개의 뷰가 존재합니다.

빨강 뷰 안에 %로 뷰 하나를 더주고, 노랑 뷰 안에 wp와 hp로 뷰를 넣어 줍시다.

<GlobalStyled.ViewCol style={{ flex: 1 }}>
	<Globastyled.ViewCol 
    	style={{ 
        	flex: 1, 
            backgroundColor: 'tomato'}} >
        <GlobalStyled.ViewCol 
        	justifyContent: 'flex-start',
            alignItems: 'flex-start',
        	style={{
            	widht: '50%'
            	height: '50%'
            }}
        />
    </GlobalStyled.ViewCol>
    
    
	<Globastyled.ViewCol 
    	style={{ 
        	flex: 1, 
            backgroundColor: 'yellow'}} >
        <GlobalStyled.ViewCol 
        	justifyContent: 'flex-start',
            alignItems: 'flex-start',
        	style={{
            	widht: wp(50),
            	height: hp(50),
            }}
        />
            
    </GlobalStyled.ViewCol>
</GlobalStyled.ViewCol>

결과는 아래와 같습니다. 차이가 보이시나요?

즉, %는 자신의 부모 뷰인 빨강 뷰를 기준으로 높이와 너비가 50%로 출력되는 것이고, wp와 hp는 가장 최상단 컨테이너, 즉 기기의 스크린 너비와 높이를 기준으로 %를 측정하여 dp로 반환하는 것의 차이입니다!

자식 뷰에서 부모 뷰와 상관없이 비율로 크기를 주고싶다면 wp와 hp를 쓰는것이 좋습니다🤩

profile
나는 시오

0개의 댓글