[꼭꼭] 우리들의 컴포넌트

NinjaJuunzzi·2022년 10월 1일
9

우아한테크코스

목록 보기
20/21
post-thumbnail

우아한 테크코스에는 코치와 커피 또는 식사를 함께 할 수 있는 코치와의 쿠폰 제도가 있습니다. 쿠폰을 받으면, 이를 준 사람에게 신청하여 커피나 식사를 함께 하는 방식으로 만남을 갖는 우아한 테크코스 만의 문화인데요. 우리는 위와 같이 쿠폰 문화와 유사한 Coffee Chat 문화가 있는 집단에서 집단에서 만남을 쉽게 하기 위해 꼭꼭을 개발하였습니다.

컴포넌트는 트레이드 오프입니다. 재사용을 위해 추상화 시켜놓는다고 해서 장점만 갖는 것도 아니며, 명시적으로 분리하여 개발한다고 해서 단점만 존재하는 것도 아닙니다. 저희는 이러한 사고 아래 가장 중요한 것은 재사용 컴포넌트를 정하는 기준이라고 생각하였습니다.

이 포스팅은 이 규칙에 대한 이야기를 하고 있습니다. 재미있게 봐주시고, 지적하실만한 부분이 있다면 댓글 혹은 이메일로 컨택해주시면 감사하겠습니다.

1. 컴포넌트 재사용 기준이 필요한 이유

  • 거시적 관점 : UI는 사용자에 의해 변화한다.

  • 미시적 관점 : 우리 프로젝트 특성 상 디자이너가 존재하지 않았기에 운영 기간에 디자인 요구 사항이 변경될 수 있다.

UI는 사용자에 의해 변화하게 됩니다. 절대적으로 좋은 UI란 존재하지 않는다고 생각하지 않습니다. 시대가 변할수 록 기술이 발전할 수록 혹은 타겟 유저층이 변함에 따라 문제 해결 영역을 잘 풀어내는 방법은 달라질 것이고, 이에 따라 UI는 변화를 맞이하게 될 것입니다.

또한 우리의 프로젝트에는 디자이너가 존재하지 않았습니다. 프론트 개발자 2인이 디자인 업무를 수행하였고, 그러다보니 어설픈 부분이 다수 존재할 수 있다고 판단하였는데요. 게다가 Feature를 빠르게 출시하여(MVP로 제작하여) 사용자들에게 실제로 우리 서비스가 필요할 지 검증 해야했기에 더욱 더 디자인에는 투자할 수 있는 시간이 많지 않았습니다.

결국 프로젝트의 특성 상 UI의 변화와 확장이 운영기간에 잦은 빈도로 발생할 수 있다고 판단했고, 이 변화와 확장을 빠르게 적용할 수 있도록 컴포넌트 재사용 케이스는 기준에 따라 명확히 정해야했습니다. 그렇다면 어떤 컴포넌트일 경우에 변화와 확장을 빠르게 적용할 수 있는 것일까요. 규칙을 말하기전 재사용을 위해 추상화된 컴포넌트명시적으로 분리된 컴포넌트의 장 단점을 정리해보겠습니다.

2. 재사용을 위해 추상화된 컴포넌트

이 경우 컴포넌트와 UI는 1:N관계이다.

1) ✅ : 빠른 개발이 가능하다. UI 코드를 두 번 작성할 필요 없이 컴포넌트 호출 만으로 UI를 표현할 수 있다. 추상화를 더 하는 일은 쉽게 상상할 수 있습니다.

2) ✅ : 커버하고 있는 UI들에 반영되야 하는 공통 변경 사항에 대해 처리하기 쉽다. 하나의 컴포넌트만 수정하면 된다.

3) ❌ : 컴포넌트 코드가 복잡할 것이다. 여러 UI를 커버하기 위해 props & state 정보가 많아지고, 이에 따라 추가적인 제어문들도 따라올 것입니다. 명시적으로 분리되어 각자의 코드만이 적혀있을 때 보다 훨씬 컴포넌트 코드의 정보량이 많아져 이해하기 어렵게 됩니다.

4) ❌ : 각 UI들에 반영되야 하는 세부 변경 사항에 대해 처리하기 어렵다. 컴포넌트 코드가 복잡할 것이기에 이를 이해하고, 처리하기란 어렵습니다. 변경 사항을 반영하면서 추가되는 제어 코드들이 존재하기에 컴포넌트 코드가 더 복잡해지는 것은 덤입니다. (하드 코딩의 단점을 모조리 얻게 되는 형태라고 생각합니다.)

5) ❌ : 수정 사항을 반영하는 시간이 오래 걸린다. 커버하고 있는 영역이 많기에 리팩터링 이후 잘 동작하는 지 테스트 해야하는 곳이 많다.

이 방식의 키 포인트는 공통 변경 사항에 대해서는 변화에 유연하지만, 세부 변경 사항에 있어서는 변화에 유연하지 않다는 것입니다. 다음은 코드 예시입니다. 즐기실 분은 코드에 달아놓은 주석과 함께 보시면 감사하겠습니다.

프로그래밍 요구 사항 : 개인에게 보여지는 게시글 리스트의 게시글 UI와 핫한 게시판에서 보여지는 게시글 UI는 비슷한 UI를 가집니다. 이를 컴포넌트로 코드로 작성해주세요

// PostItem.tsx

// 단점
// 1. 변경 사항이 발생하게 되면, 잘 돌아가는 지에 대해 테스트 할 곳이 많기에 대응이 느리다.
// 2. 복잡한 코드가 된다. 마찬가지로 호출 시점에 이해해야하는 정보도 줄어든다.
// 3. 공통 변경 사항을 반영하기엔 수월하다. 하지만 테스트 해야할 영역은 많아진다.



// 장점
// 1. 공통 변경 사항을 처리하기 수월하다. (스타일을 수정하거나, 마크업 구조를 변경하는 등의)
// 2. 빠른 개발이 가능하다. 마크업과 스타일을 또 선언하고, 기능을 구현하는 시간이 줄어들게 된다.
const PostItem = ({type} : PostItemProps) => {
  if(type === 'HOT'){
   	return ... 
  }
    
  return ...
}

3. 명시적으로 분리된 컴포넌트

이 경우 컴포넌트와 UI는 1:1 관계입니다.

1) ✅ : 빠른 개발이 가능하다. 가장 원시적이지만 가장 강력한 복사 붙여넣기 기능을 활용하여 서로 다른 부분에 대해서만 코드를 작성하기에 추상화된 컴포넌트와 마찬가지로 빠른 속도로 개발이 가능하긴 합니다.

2) ✅ : 세부 변경 사항을 각자 적용하기 편리합니다. 로직은 각 영역에서만 추가되고 변경되면 됩니다. 각자의 스코프를 갖기에 서로에게 영향을 미치지 않는 존재가 되는 방식이기에 변경 사항 적용 후 서로를 디버깅할 필요도 사라지게 됩니다.

3) ✅ : 가독성이 좋다. 하나의 UI만을 대응하고 있기 때문에 제어 로직도 복잡해질 일이 없습니다. 코드를 읽기 수월해지기에 유지보수에 뛰어난 코드가 됩니다.

4) ❌ : 공통 변경 사항을 처리하기 어렵습니다. 모든 영역에 적용되어야 하는 변화의 경우 각 영역에 대하여 반복 작업을 수행해야하기에 휴먼에러 가능성도 농후하고, 시간도 추상화된 방식에 비해 비교적 더 들어가게 됩니다.

이 방식의 키 포인트는 세부 변경 사항에 대해서는 변화에 유연하지만, 공통 변경 사항에 있어서는 변화에 유연하지 않다는 것입니다. 다음은 코드 예시입니다. 즐기실 분은 코드에 달아놓은 주석과 함께 보시면 감사하겠습니다.

프로그래밍 요구 사항 : 개인에게 보여지는 게시글 리스트의 게시글 UI와 핫한 게시판에서 보여지는 게시글 UI는 비슷한 UI를 가집니다. 이를 컴포넌트로 코드로 작성해주세요

// PostItem.tsx

// 단점
// 1. 공통 변경 사항에 대응하고자 한다면 코드 작업을 두 배로 해주어야한다.

// 장점
// 1. 변경 사항이 발생하더라도 잘 돌아가는지 확인해야하는 영역이 적다.
// 2. 마찬가지로 빠른 개발은 가능하다.
// 3. 복잡한 코드가 작성되지 않아도 된다. 마찬가지로 호출 시점에 이해해야하는 정보도 줄어든다.
// 4. 세부 변경 사항을 반영할 때 유리하다.
const PostItem = () => {
    
  return ...
}
  
PostItem.Hot = () => {
  
  return ...  
}

4. 정리

  • 재사용되는 컴포넌트 : 공통 변경 사항의 등장이 잦고 처리가 어려울 때 작성한다.

  • 분리된 컴포넌트 : 세부 변경 사항의 등장이 잦고 처리가 어려울 때 작성한다.

재사용 컴포넌트의 경우 공통 변경 사항을 한 번만 적용하면 되기에 유연하게 처리될 수 있지만, 변경 사항 반영 후 테스팅 해야할 곳이 많고, 세부 변경 사항은 적용하기 어렵다라는 단점이 존재합니다. 코드가 복잡해지기에 가독성도 떨어질테고, 사용하기 위해 이해해야하는 정보는 많아지는 것이죠.

분리된 컴포넌트의 경우 공통 변경 사항을 각 컴포넌트 별로 처리해주어야 하지만, 변경 사항 반영 후 테스팅 해야하는 곳이 적고, 세부 변경 사항에 대응하기 쉽다는 장점이 존재합니다. 코드가 단순해져 가독성이 올라가고, 이해해야하는 정보도 줄어들게 되겠지요.

저희 팀의 기준은 위 사고 아래에서 만들어졌습니다. 다음은 기준과 그 이유에 대한 이야기입니다.

5. 재사용 기준과 그 이유

  • 도메인 영역이 같은가?

  • 같은 사용자 경험을 제공하는 UI인가?

이 두 가지 규칙에 따라 컴포넌트를 재사용 할지 말지 토의하고 있습니다. 두 가지 규칙에서 어긋난다면 명시적으로 분리하고 있다는 것인데요. 도메인이 같으면서 경험이 동일한 경우 토의 결과 세부 변경 사항에 대해서 쉽게 상상할 수 없다면 공통 변경 사항을 빠르게 처리할 수 있도록 컴포넌트를 재사용합니다.

하지만 도메인이 다른 경우 혹은 제공하고자 하는 사용자 경험이 다른 경우 라면 세부 변경 사항에 더 빠르게 대응하고, 변경 후 테스팅 영역을 줄일 수 있도록 분리된 컴포넌트로 개발하고 있습니다. 이를 좀 더 자세히 말씀드려보겠습니다.

1) 도메인 영역이 같지 않다면 분리된 컴포넌트로 개발

도메인이란 문제 해결 영역을 말합니다. 문제 영역을 설정하다 보면 분리되는 영역이 생기기 마련입니다. User 영역이 생길 수도 있고, 저희와 같은 서비스 도메인이라면 Coupon 이라는 도메인 영역이 생길 수도 있습니다.

이렇게 도메인은 세부 도메인 영역으로 나뉠 수 있는 것인데요. 저희는 이렇게 생기는 세부 도메인 영역이 서로 다르다면 비슷한 UI 이더라도 다른 컴포넌트로 개발하는 전략을 가져가고 있습니다. (모든 경우에 그러한 것은 아닙니다. 재사용 해야하는 이유가 있다면 재사용 컴포넌트로 활용하고 있는 경우도 있습니다.)

그 이유는 다음과 같습니다.

준찌와 시지프 : 세부 도메인이 다르다면, 문제 해결 영역이 다르다는 것이다. 그렇다면 각 영역을 사용자에게 UI로 잘 표현할 수 있는 방법은 서로 다르다.

도메인 : UnregisteredCoupon 영역에서의 쿠폰 UI

도메인 : Coupon 영역에서의 쿠폰 UI

위 UI들은 굉장히 유사합니다. (UI의 유사함을 보는 시각은 주관적일수 있으니 서로 비슷하다고 가정해보겠습니다.) 그렇기 때문에 하나의 컴포넌트로 추상화하여 두 개의 UI를 커버하도록 컴포넌트 코드를 작성할 수 있는데요. 저희는 이러한 상황에서 서로 다른 도메인 영역이기에 다른 컴포넌트로 만들어 개발합니다.

도메인 영역이 다르다는 것은 도메인 영역에 포함되어 있는 속성이 다르고, 정보가 다름을 의미합니다. 포함되어 있는 정보가 같다고 한들 서로 다른 기획 속에서 탄생한 분리된 영역이라고 생각하고 있습니다. 따라서 서로 다른 기획적 속성 및 데이터 필드를 잘 녹여낼 수 있는 방법은 지금은 같을지 몰라도 미래에는 바뀔 수 있다고 생각하기에 분리된 컴포넌트로 작성하고 있습니다.

위 도메인 영역을 좀 더 자세히 살펴봅시다. Coupon 도메인 영역은 받는 사용자가 정해져 있는 일반 쿠폰 도메인을 의미합니다. 반대로 UnregisteredCoupon 도메인 영역은 받는 사용자가 정해져 있지 않은 미등록 쿠폰 도메인을 의미합니다. 이 둘은 서로 다른 정보와 속성을 갖습니다.

  • Coupon : 받는 사람 정보가 포함됨. 일반 쿠폰이라는 속성

  • UnregisteredCoupon : 받는 사람의 정보가 없음. 받는 사람이 나중에 정해지는 특수한 쿠폰이라는 속성.

이렇게 다른 영역이었기에 현재는 비슷한 UI로 서로의 도메인을 풀어낼지 몰라도 운영간에 서로 달라질 수 있는 여지가 많다고 판단하였습니다. 미등록 쿠폰은 받는 사용자가 없으므로 From 란이 비어버릴 수도 있고, 반대로 아예 미등록 쿠폰임을 강조하기 위해 공통의 쿠폰 속성을 제거해버릴 수도 있습니다. 이렇게 변화를 상상하기 쉽다면, 역시나 분리하는 것이 옳다고 판단하기에 저희 꼭꼭팀은 컴포넌트 분리 작성을 진행하고 있습니다.

정리 : 도메인이 다르다 -> 서로 다른 속성과 정보를 갖는다 -> 서로 다르게 발전할 수도 있다. -> 세부 변경 사항이 발생한다 -> 세부 변경 사항에 유연하게 처리할 수 있도록 분리된 컴포넌트로 가져간다.

2) 같은 사용자 경험을 제공하는 UI가 아니라면 분리된 컴포넌트로 개발

꼭꼭팀은 도메인 영역이 다른 경우 분리된 컴포넌트로 개발을 진행한다고 말씀드렸습니다. 그렇다면 모든 케이스에 대하여 도메인이 같다면 하나의 컴포넌트를 재사용한다는 것일까요? 그건 아닙니다. 저희는 도메인 영역이 같은 경우에도 하나의 기준을 추가해서 토의를 진행합니다. 같은 사용자 경험을 제공하는 UI인가 에 대한 토의를 진행 후 컴포넌트를 재사용할지 분리할지에 대해 결정합니다.

도메인 영역: 쿠폰 영역 (쿠폰 UI)

도메인 영역: 쿠폰 영역 (쿠폰 생성 Form에서의쿠폰 Preview UI)

위 UI를 보시면 서로 비슷하게 보입니다. 따라서 충분히 하나의 컴포넌트로 두 개의 UI 영역을 커버할 수 있어보입니다. 하지만 저희는 이러한 경우에도 세부 변경 사항에 빠르게 대응할 수 있도록 컴포넌트 분리를 시도합니다. 그 이유는 다음과 같습니다.

  • 주고자 하는 사용자 경험이 풀어내는 UI 역시 서로 달라질 수 있는 영역이다.

가령 preview 라는 경험을 더 효과적으로 주기위해 애니메이션을 추가한다거나, 이미지의 느낌을 살리려 다른 이미지를 사용한다는 등의 세부 수정 사항이 발생한다면 컴포넌트 재사용 시 이는 처리하기 매우 힘들 것 입니다. 서로의 영향이 없는 지 체크해주어야하고, 애초에 두 UI를 처리하고 있는 복잡한 코드를 이해한 후 그곳에다 변경을 반영해야하기 때문입니다. 따라서 세부 변경 사항이 발생할 수 있고, 이를 쉽게 대응할 수 있게 분리된 컴포넌트로 개발합니다.

하지만 같은 도메인 영역이라면 공통 변경 사항 역시 발생할 수 있기에 최대한 공통 변경 사항에도 쉽게 대응하기 위해 다음과 같이 스타일을 공유하고 있습니다

import * as Styled from './style';

const CouponItem = (props: BigCouponItemProps) => {
  const { className, onClick, ...coupon } = props;

  const { couponTag, couponStatus, couponMessage, meetingDate, thumbnail } = {
    ...coupon,
    thumbnail: THUMBNAIL[coupon.couponType],
  };

  const { isSent, member } = useCouponPartner(coupon);

  return (
    <Styled.Root className={className} hasCursor={!!onClick} onClick={onClick}>
      <Styled.CouponPropertyContainer>
        <CouponStatus status={couponStatus} meetingDate={meetingDate} isSent={isSent} />

        <Styled.ImageInner>
          <img src={thumbnail} alt='쿠폰' width={44} height={44} />
        </Styled.ImageInner>
      </Styled.CouponPropertyContainer>
      <Styled.TextContainer>
        <Styled.Top>
          <Styled.Member>
            <Styled.English>{isSent ? 'To.' : 'From.'}</Styled.English> {member.nickname}
          </Styled.Member>
        </Styled.Top>
        <Styled.Message>{couponMessage}</Styled.Message>
        <Styled.Hashtag>#{couponTag}</Styled.Hashtag>
      </Styled.TextContainer>
    </Styled.Root>
  );
};


// Preview 컴포넌트
CouponItem.Preview = function Preview(props: BigCouponItemPreviewProps) {
  const { className, receiver, couponTag, thumbnail, couponMessage } = {
    ...props,
    thumbnail: THUMBNAIL[props.couponType],
  };

  return (
    <Styled.Root className={className} hasCursor={false}>
      <Styled.ImageContainer>
        <Styled.ImageInner>
          <img src={thumbnail} alt='쿠폰' width={44} height={44} />
        </Styled.ImageInner>
      </Styled.ImageContainer>
      <Styled.TextContainer>
        <Styled.Member>
          <Styled.English>To.</Styled.English> {receiver.nickname}
        </Styled.Member>
        <Styled.Message>{couponMessage}</Styled.Message>
        <Styled.Hashtag>#{couponTag}</Styled.Hashtag>
      </Styled.TextContainer>
    </Styled.Root>
  );
};

이런식으로 하나의 스타일을 공유하여 공통 변경 사항에 최대한 빠르게 대응할 수 있도록 분리하였습니다. (스타일을 공유함으로써 공통 스타일 변경 사항에 빠르게 대응할 수 있다. 또한 각자의 경험이 유지된다. 세부 변경 사항에도 빠르게 대응할 수 있다.)

3) 도메인 영역이 같으면서 의도하는 사용자 경험이 같다면 재사용한다.

도메인 영역이 같으면서 의도하는 사용자 경험이 같다면 재사용합니다. 저희의 재사용 케이스는 CouponList 컴포넌트 였습니다. 위 두 조건을 만족했기에 각자의 방향으로 변경될 것이라기 보단, 변경 사항이 재사용되는 모든 곳에 반영되어야 한다고 판단하였습니다.

  • 메인 페이지에서의 CouponList

  • 쿠폰 리스트 페이지에서의 CouponList

이 경우 리스트에 변경 사항이 발생해야 한들 어느 한 곳에만 적용될 것을 상상하기란 쉽지 않았습니다. 팀 단위로 논의를 진행해보았지만 그렇다할 재사용했을 때의 위험성이 드러나지 않아서 이는 재사용하고 있습니다.

결론

이렇듯 저희 꼭꼭팀은 컴포넌트의 재사용 기준을 잡아두고 토의를 진행한 이 후 컴포넌트를 재사용할 지 분리된 컴포넌트로 새롭게 만들어낼 지를 정하고 있습니다. UI는 언제나 변화하고, 이 변화에 가장 빠르게 대응하는 것이야말로 좋은 사용자 경험을 추구해야할 서비스 개발자의 덕목이라 믿고 있기에 무작정 추상화하여 하드 코딩을 하기보단 우리만의 규칙을 정해 컴포넌트를 개발하고 책임지는 구조로 프로젝트를 진행하고 있습니다.

재미있게 읽어주셨다면 하트 눌러주시면 감사하겠습니다. 아직도 팀에서 재사용 기준 없이 반복되는 컴포넌트 코딩 작업만 하고 있는 사람들에게 도움이 되었으면 합니다!! 모두들 파이팅~~!

profile
Frontend Ninja

1개의 댓글

comment-user-thumbnail
2022년 10월 22일

멋져요!!!

답글 달기