다짐) 디자인 시스템 설계에 대한 고민들 (Part 1. 뷰와 로직의 분리)

2ast·2023년 8월 19일
0

디자인 시스템을 구축해보고 싶다!

다짐을 React Native로 새롭게 구축하는 알보칠 프로젝트 진행이 결정되고 가장 먼저 해보고 싶었던 건 바로 '디자인 시스템 구축'이었다. 사실 디자인 적으로는 이미 Figma에 디자인 시스템이 정리되어 있던 상태였다. 하지만 사내 구축된 RN 프로덕트에서 개발 시점에 적극적으로 쓰이고 있지는 않았다. 단지 디자인시스템을 참고만 할 뿐 그때그때 필요한 컴포넌트를 만들어서 사용하고 있었다. 마침 그 시점이 디자인 시스템에 대한 관심이 최고치에 달했던 때라 "이번 기회에 내가 RN으로 다짐의 디자인 시스템을 구축해보겠다"고 호기롭게 다짐했었다. 사실 디자인 시스템 구축 경험도 없이 시작한 만큼 많은 시행착오도 겪었고, 지금의 디자인 시스템이 완성도 높다고도 말할 수 없다. 하지만 그럼에도 불구하고 그 과정에서 깨달은 점을 정리해서 공유해보고 싶다.

뷰와 로직 분리하기

뷰와 로직의 분리는 내가 디자인 시스템을 구축하면서 가장 많이 실수했던 부분인 동시에, 현재까지도 가장 의식하면서 고려하는 부분이다. 디자인 시스템이란 무엇인가? 우리 서비스의 디자인적 정체성을 나타내는 요소를 공통점과 재사용성의 관점에서 추출하고 유형화한 것이 디자인 시스템이다. 그런 관점에서 본다면 바로 '재사용성'과 '확장성'이야말로 개발자가 가장 신경써야하는 속성이다. 그리고 난 이를 가장 크게 저해하는 요소가 바로 '혼재된 비즈니스 로직'이라고 생각한다. 내가 아바타 컴포넌트를 만들며 겪은 사례를 조금 각색하여 그 예시를 들어보겠다.

Avatar component

다짐의 디자인 시스템에는 Avatar 컴포넌트가 정의되어 있다. 일반적으로는 단지 동그란 레이아웃에 profile image가 보여지는 형태지만, 일부 스크린에서는 오른쪽과 같이 카메라 아이콘이 노출되어 눌렀을 때 프로필 사진을 변경할 수 있는 인터페이스를 제공해줘야 한다.

초기 Avatar Component

맨 처음 Avatar component를 만들 때는 컴포넌트 내부에 프로필 변경 로직을 함께 작성해 주었다.

const Avatar = ({
  ...
  editable,
}: AvatarProps) => {
  
  const onSelectImage =(image)=>{
  	setProfileImage(image)	
  }
  
  const onEditPress = () =>{
  	openImagePicker({
    	onComplete:onSelectImage
    })
  }
  
  
  return (
    <AvatarContainer
      ...
      disabled={!editable}
      onPress={onEditPress}>
      ...
      {editable && <CameraButtonWhenEditableProfile />}
    </AvatarContainer>
  );
};

export default Avatar;

현재로써는 editable을 활성화하는 케이스가 오직 마이다짐의 프로필 이미지 변경 밖에 없기 때문에 이 코드는 아무런 문제가 없다. 하지만 만약 프로필 종류가 다각화되거나 아바타를 사용하는 신규 기능이 출시되어 api 분기처리가 필요해지면 어떡해야 할까? 이런 고민들이 이어진 결과 현재 코드는 확장성에 불리한 구조라고 판단했다. 따라서 아래와 같이 onSelectImage 함수를 props로 받아 이미지 선택 이후의 로직을 스크린에 일임하기로 했다.

중기 Avatar Component

const Avatar = ({
  ...
  editable,
  onSelectImage
}: AvatarProps) => {
  
  const onEditPress = () =>{
  	openImagePicker({
    	onComplete:onSelectImage
    })
  }
  
  
  return (
    <AvatarContainer
      ...
      disabled={!editable}
      onPress={onEditPress}>
      ...
      {editable && <CameraButtonWhenEditableProfile />}
    </AvatarContainer>
  );
};

export default Avatar;

onEditPress를 여전히 컴포넌트 내부에서 정의했던 것은 어차피 아바타를 눌러 갤러리를 여는 동작은 일관적이라는 안일한 판단에서 기인했다. 그리고 얼마 지나지 않아 이 선택을 번복해야했다. 기획이 조금 수정되어 아래와 같이 바텀시트가 노출되야 했기 때문이다.

게다가 이후 다짐의 디자인 시스템을 서브모듈로 빼서 다짐파트너로 이식하는 작업을 진행했는데, 다짐파트너에서 사용하는 image picker 라이브러리가 다짐에서 사용하는 것과 달라 충돌이 발생하는 헤프닝도 있었다. 이 일을 계기로 디자인 시스템은 오직 뷰를 보여주는 역할에만 충실하고 그 이외의 비즈니스 로직은 모두 컴포넌트를 가져다 쓰는 곳에 일임하자는 원칙이 확고하게 자리잡게 되었다.(역시 머리로 아는 것과 맞고 나서 깨닫는 건 같지 않은 법이다.)

현재 Avatar Component

const Avatar = ({
  ...
  editable,
  onEditPress
}: AvatarProps) => {
  
  return (
    <AvatarContainer
      ...
      disabled={!editable}
      onPress={onEditPress}>
      ...
      {editable && <CameraButtonWhenEditableProfile />}
    </AvatarContainer>
  );
};

export default Avatar;

로직을 모두 제거하고 뷰만 남은 아바타 컴포넌트의 모습이다. 만약 대부분의 케이스에서 비슷한 로직을 사용하기 때문에 뷰와 로직의 분리로 발생하게 될 코드 중복이 탐탁치 않다면 차라리 custom hook을 따로 만들 것을 추천한다.

const EditProfile = () =>{
	...
    const onEditPress = useEditProfileAvatar()
  
    return <Container>
      ...
      <Avatar editable onEditPress={onEditPress}/>
      ...
    </Container>

}

뷰로 로직의 분리 관점 때문은 아니지만, 실제로 다짐의 Input component는 useValidationText라는 custom hook과 함께 사용하고 있기도 하다.

const {
  text: phoneNumberText,
  setText: setPhoneNumberText,
  errorText: phoneNumberErrorText,
  setErrorText: setPhoneNumberErrorText,
  inputRef: phoneNumberInputRef,
  focusInput: focusPhoneNumberInput
} = useValidationText({[
  {regex: Regex.phone, message: '올바른 연락처를 입력해 주세요.'},
]);


<DgInput
  text={phoneNumberText}
  setText={setPhoneNumberWithHyphen}
  errorMessage={phoneNumberErrorText}
  ref={phoneNumberInputRef}
/>

Part 2. 분리와 결합에서 계속

profile
React-Native 개발블로그

2개의 댓글

comment-user-thumbnail
2023년 8월 19일

뷰 로직 분리에 대한 좋은 글 잘 보고 갑니다 :)

1개의 답글