오빠 톤 많아? v1.0.0 배포하기까지의 개발 과정

설탕·2023년 5월 2일
11
post-thumbnail

✏️ 이 글은 오빠! 톤 많아? w 테오의 스프린트 14기에 이어지는 글로, 오빠 톤 많아? 퍼스널 컬러 자가진단 서비스 개발 과정을 담고 있습니다.

테오의 스프린트 그 후...

계속 참여 가능한 팀원들끼리 프로젝트를 이어서 진행했다.
기존 팀원 5명 중 4명과, 아이디어와 레퍼런스를 제공해주신 soojjung님을 초대해서 5명이서 진행했다.

v1.0.0을 위한 목표 설정 🎯

테오의 스프린트 회고에서 이야기가 나왔던 우리 서비스에서 개선하고 싶은 점을 모아보았다.

스프린트를 계속 진행하며 이 중 필요하고 실현 가능한 것을 골라 하나씩 구현하여 v1.0.0 배포하는 것을 목표로 세웠다.

Top Priority: 퍼스널 컬러 유형 12타입으로 확장

가장 먼저 해야 할 것은 기존에 데모를 위해서 퍼스널 컬러 유형을 우선 봄, 여름, 가을, 겨울 이렇게 4가지 유형으로만 구성했었는데, 이 4타입을 12타입으로 확장하는 것이었다. 퍼스널 컬러 유형은 흔히 계절별로 각각 3타입씩―예를 들면 봄 계절에는 봄웜, 봄라이트, 봄브라이트 이렇게―총 12타입으로 구분되어 있었다.

그러기 위해서는 결과 데이터도 12타입으로 늘리고, 테스트에서 컬러를 선택하는 로직도 다시 구성해야 했다. 그리고 무엇보다도 12유형 확장과 더 정확한 결과 데이터를 위해 레퍼런스를 찾아야 했기에... 퍼스널 컬러 책도 보고 외국 사이트도 찾아보고 도메인에 대한 공부를 많이 하게 되었다. 😂

버저닝을 고민하다가 데모 버전은 프로토타입이므로 버전에 포함하지 않기로 결정해서 v1.0.0을 새로 배포하기로 했다.

계속되는 스프린트와 프로젝트 보완 🏃🏻

일주일 단위의 스프린트

v1.0.0 배포라는 큰 목표를 달성하기 위한 작은 목표들을 매 주마다 설정하고 주 단위의 스프린트를 계속하면서 작은 목표들을 달성해나갔다.

당장 이번 주에 할 일을 정하고, 일주일 동안 작업하고, 일주일 후 미팅에서 작업 진행도를 공유하며 미완료 작업은 그 다음 주 스프린트로 넘기고 새로운 할 일을 추가해서 다음 주 스프린트를 이어가는 방식으로 약 한 달간 프로젝트를 이어서 진행했다.

JavaScript에서 TypeScript로 마이그레이션

TypeScript 도입해? 말아?

타입스크립트를 강의 신청해놓고 아직 안 들어서 구글링으로 배운 나는 프로젝트에 TypeScript를 적용하는 컨벤션을 잘 몰라서 망설였는데, 팀원분들이 도와주신다고 TypeScript 적용해보자고 하셔서 TypeScript로 전환하게 됐다!

어떻게 바꿨는데?

  • js, jsx 파일들을 ts, tsx로 변환
  • type 선언 추가
  • type 예외 처리 코드 작성
if (typeof result === 'string') {
  setImage(result);
  return;
}

throw Error('이미지 파일을 불러오는 데 오류가 발생했습니다.');

에러 수집을 위한 Sentry 도입

도입 이유

개발하면서 발생하는 에러는 개발자가 직접 확인할 수 있지만, 배포된 서비스를 사용자가 사용하면서 발생하는 에러는 개발자가 알 수 없다.
서비스 사용 시 발생하는 에러를 수집하기 위해 Sentry를 도입했다. 물론 개발 단계에서 발생하는 에러도 수집하고 분석할 수 있다.

에러 원인 분석 및 해결

실제로 개발 단계에서 내 맥북에서 결과 페이지의 공유하기 버튼을 눌렀을 때 다음과 같은 에러가 발생했다. 모바일이나 다른 팀원 기기에서는 에러 없이 잘 작동하는데 내 맥북에서만 이런 에러가 발생하는 것이었다.

Sentry를 도입한 팀원 준이 원인과 해결책을 찾아주셨다.
Sentry에서는 아래와 같이 에러가 발생한 디바이스와 브라우저 환경을 확인할 수 있다.

내가 접속한 크롬 + Mac OS 조합에서는 Web Share API를 지원하지 않는 것이 원인이었다. 이 조합에서는 API를 호출하지 않고 메시지를 띄우는 것으로 예외 처리를 했다.

import { UAParser } from 'ua-parser-js';

function isOSX() {
  const parser = UAParser(navigator.userAgent);
  const os = parser.os;

  return os.name === 'macOS';
}

function isChrome() {
  const parser = UAParser(navigator.userAgent);
  const browser = parser.browser;

  return browser.name === 'Chrome';
}

const handleShare = async () => {
  if (isChrome() && isOSX()) {
    alert(
      'macOS 환경의 크롬 브라우저에서는 지원하지 않는 기능입니다.\n다른 브라우저에서 실행해 주세요. 🥰'
    );
  } else {
    await webShare();
  }
};

커스텀 에러를 설계하면 더 체계적으로 에러 핸들링을 할 수 있다.
준이 작성한 에러 핸들링 문서를 참고하여 추후 커스텀 에러를 추가해나갈 예정이다.

친구 공유(사진 포함) 기능: 내 얼굴로 다른 사람이 진단해줄 수 있을까?

결론부터 말하면 법적인 문제로 현재로선 불가능하다고 판단해서 이 기능은 구현 목록에서 제외했다.

결과 공유 시 얼굴 사진이 포함되도록 해서 친구가 내 사진으로 진단해주는 기능이 있으면 좋겠다는 의견이 있었다. 이 서비스가 내 생각대로 어울리는 컬러를 선택해서 진단해보는 서비스이기 때문에 다른 사람들이 함께 선택해주면 확실히 도움이 되긴 할 것이다.

하지만 얼굴 사진을 다른 사람에게 공유하려면 사진을 서버에 저장해야 한다. 사진을 수집하기 위해서는 개인정보 수집 동의를 받아야 한다. 그러려면 동의 약관도 있어야 할 것이고... 일정 기간 뒤에는 사진을 파기해야 할 것이고... 사진이 수집된다고 하면 사용자가 본인 얼굴 사진을 올리는 것을 꺼려할 수도 있다.

코드 외적으로 해결해야 할 과정이 너무 복잡하고 현재로서는 그 정도의 노력을 투자할 정도로 필요한 기능이 아니기에 이 기능은 추가하지 않기로 결정했다.

내가 한 주요 작업 👩🏻‍💻

다음은 내가 한 일 중 주요 작업들이다.

UX 개선: 이미지 깨짐 현상 방지

문제

사용자가 업로드한 얼굴 사진 데이터를 recoil 전역 상태로 저장하고, 컬러 선택 페이지에서 사용하고, 결과 페이지로 이동할 때 recoil에 저장된 이미지 데이터를 초기화했다.

그런데 결과 페이지에서 뒤로가기를 하면 이미 recoil에서 초기화했기 때문에 이미지 데이터가 없어서 깨진 화면이 나오는 것이 매우 거슬렸다. 보기에도 안 좋고, 사용자가 결과를 확인했다가 다시 컬러 선택 화면으로 돌아가고 싶을 수도 있는데 이미지가 없으면 다시 테스트할 수도 없다.

깨진 화면이 안 보이게 해보자!

해결

  • recoil 이미지 데이터 초기화 시점: (변경 전) 컬러 선택 다 하고 결과 페이지 이동 시 → (변경 후) 랜딩 페이지 렌더링 시
  • 404 Not Found 경로에 첫 화면으로 돌아갈 수 있는 커스텀 페이지 추가
  • 이미지 데이터가 없는 상태에서 컬러 선택 페이지에 들어가면 404 Not Found 페이지로 리다이렉트하는 커스텀 훅 추가
// Landing/index.tsx
import { useSetRecoilState } from 'recoil';

function LandingPage() {
  const setUserImg = useSetRecoilState(CropImage);

  useEffect(() => {
    setUserImg('');
  }, [setUserImg]);
  ...
}
// Router.tsx
function AppRouter() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path={landing} element={<LandingPage />} />
        ...
        <Route path="/*" element={<WrongAccessPage />} />
      </Routes>
    </BrowserRouter>
  );
}
// useRedirectNoImage.ts
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

const useRedirectNoImage = (userImg: string) => {
  const navigate = useNavigate();

  useEffect(() => {
    if (userImg) return;

    navigate('/no-image');
  }, [navigate, userImg]);
};

export default useRedirectNoImage;

이제는 결과 페이지에서 뒤로 가기해서 컬러 선택 페이지로 이동해도 이미지가 그대로 있어서 테스트를 다시 할 수도 있고, 이미지 데이터가 초기화된 후에 앞으로 가기/뒤로 가기를 통해 컬러 선택 페이지에 접근하더라도 깨진 화면을 보는 대신 404 커스텀 페이지를 통해 처음으로 되돌아갈 수 있다.

이미지 파일 public 폴더에서 src 폴더로 이동

기존에는 이미지 파일을 public 폴더에서 관리하고 있었는데, 이미지를 public 폴더보다 src 폴더에서 관리하는 것이 에러 핸들링과 최적화에 유리하다는 것을 알게 되어 리팩토링을 했다.

이미지 파일을 src/assets 폴더로 옮기고, 변수로 import해서 사용하도록 바꾸었다.

그런데 타입스크립트에서 이미지 파일을 인식하지 못해서 import 에러가 발생했다.
이미지 파일 확장자를 전역 모듈로 선언해주니 해결되었다.

// index.d.ts
declare module '*.jpg';

이때 alias path 설정하는 방법도 알게 되어서 import 상대 경로는 이후에 절대 경로로 바꾸었다.

컬러 선택 로직 설계

한 단계에는 4개의 선택지가 있다. 이 선택지에는 각각 4계절을 보여준다. 각 계절마다 3가지 유형이 있으므로, 3단계를 진행하면 12타입을 모두 선택지에 보여줄 수 있다.
단계가 너무 적으면 결과의 정확도가 떨어지고, 단계가 너무 많으면 사용자가 귀찮고 피곤해진다. 팀원들과 논의한 끝에 총 9단계가 적정하다는 결론을 내렸다. 4선택지 × 9단계 = 12유형 × 3번이므로 9단계면 한 유형당 3번씩 선택지에 노출된다.

퍼스널 컬러 선택 결과를 도출하는 기본 로직은 최빈값이다. 가장 많이 선택한 유형이 최종 결과가 된다. 그런데 9번 선택했을 때 중복 최빈값이 발생할 수 있다. 9단계가 끝난 후 봄웜 2회 선택, 여름쿨 2회 선택으로 최빈값이 2가지 이상이 될 수도 있다는 말이다. 이렇게 중복 최빈값이 발생했을 때 이 중에서 최종 결과를 가려내기 위해 보너스 단계를 추가하기로 했다.

보너스 단계 추가

보너스 단계는 9단계까지 진행했을 때 가장 많이 선택된 유형들로 선택지가 구성된다. 그리고 이 보너스 단계에서 선택한 유형이 최종 결과가 된다.

그런데 최악의 경우에는 사용자가 9번 모두 다른 선택을 해서 12유형 중 9유형이 최빈값이 될 수도 있다. 이 경우 어떻게 할 것인지 논의했는데 (팀원들: 이러면 테스트 다시 해야지~!) 어차피 이 경우는 이미 결과의 신뢰성을 보장할 수 없게 되었다고 판단해서, 한국인 퍼스널 컬러 유형 중 가장 흔한 타입 4가지를 추려내어 선택지를 구성하도록 했다.

또 9단계에서 이미 최빈값이 1가지 유형으로 결정된 경우에도 보너스 단계를 진행할지 논의한 결과 의미가 없다고 판단하여, 보너스 단계 없이 바로 결과 페이지로 이동하도록 했다.
그래서 이후에 배포하고 사람들이 같이 해볼 때 바로 결과 나온 사람이 다른 사람 하는 거 보면서 "뭐야 난 왜 보너스 단계 없어?!"하면서 서운해했다고... 하지만 의미 없는 보너스 단계를 추가할 이유는 되지 못한다

보너스 단계는 UI 통일성을 위해 중복 최빈값이 몇 가지든 4선택지로 고정하되(중복 최빈값이 4가지 넘는 경우는 9번 모두 다른 선택인 경우밖에 없다), 결정적인 선택이기 때문에 한 선택지에 3색을 보여주도록 했다. 색을 원그래프처럼 하나의 element에 보여주기 위해 CSS conic-gradient 속성을 이용했다.

background: conic-gradient(
  ${({ colors }) => colors[0]} 120deg,
  ${({ colors }) => colors[1]} 120deg 240deg,
  ${({ colors }) => colors[2]} 240deg
);

컬러 선택지 위치 랜덤화

기존에는 봄 여름 가을 겨울 순서대로 컬러 선택지가 보였지만, 계절을 예상하고 컬러를 선택하는 것을 방지하기 위해 선택지 위치를 랜덤으로 섞는 기능을 추가했다.
ChatGPT가 짜준 배열 랜덤 셔플하는 함수를 컬러 선택 단계 매 렌더링마다 호출해서 선택지 배열을 섞게 했다.

function shuffle(array: ChoiceColorDataType[]) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }

  return array;
}

모바일 viewport height 실제 화면 높이에 맞게 조정

우리 서비스는 모바일 우선 레이아웃을 적용하고 있는데, 모바일 기기에서 주소창 등 브라우저 내 상단/하단 메뉴 때문에 높이를 100vh로 설정한 화면에서 스크롤이 생기는 현상이 발견됐다.

React 모바일 웹 앱 100vh 실제 화면 크기로 맞추기 글을 참고해서 해결했다.

  1. 커스텀 훅을 만들어서 공통 레이아웃 컴포넌트 최상단에 넣어주었다. 그런데 이 방법은 렌더링 이후 실행되므로 나중에 더 좋은 방법을 찾아봐야겠다.
// useViewportHeight.ts
import { useEffect } from 'react';

function setViewportHeight() {
  const vh = window.innerHeight * 0.01;
  document.documentElement.style.setProperty('--vh', `${vh}px`);
}

const useViewportHeight = () => {
  useEffect(() => {
    setViewportHeight();
  }, []);
};

export default useViewportHeight;
// MobileLayout.tsx
import useViewportHeight from '@Hooks/useViewportHeight';

function MobileLayout() {
  useViewportHeight();
  ...
}
  1. CSS 최상위 파일에 변수 선언해 주고
// GlobalStyle.ts
const GlobalStyle = createGlobalStyle`
  :root {
    --vh: 100%;
  }
  ...
`
  1. height: 100vh;를 다음 코드로 바꿨다.
height: calc(var(--vh, 1vh) * 100);
/* --vh 변수로 높이를 계산하고, --vh 값이 유효하지 않을 때만 vh로 계산한다. */

v1.0.0 배포 🎉

이외에도 스타일 수정, 오류 해결, 컬러 데이터 추가 등 크고 작은 부분들을 보완하여 드디어 4월 5일 v1.0.0을 배포했다! 그리고 약 한 달간 방학😇을 가진 뒤 다음 버전을 준비할 계획이다.

방학 숙제: 피드백 수집 및 에러 모니터링

열심히 만들어서 배포했으니 이제 주변에 홍보하고 피드백을 받아오는 것이 숙제다. 팀 디스코드에 피드백 채널을 개설해서 각자 받아온 피드백을 함께 수집했다.

테오도 직접 사용해본 후 피드백을 주셨다! ❤️

개발할 때 발견되지 않았던 에러도 Sentry에 착실하게 기록되고 있다. 😂
무료 계정은 30일 전 에러까지만 기록을 보여주기 때문에 기록이 사라지기 전에 캡쳐해두거나 해결을 해야 할 것 같다.

다음 버전에서는...

  • 피드백 반영해서 개선
  • Sentry로 수집한 에러 해결
  • 나중에 해보고 싶었던 것 적용해보기: 테스트 코드, SEO, 성능 개선 등

을 계획하고 있다.

to be continued ... 🫧

profile
공부 기록

2개의 댓글

comment-user-thumbnail
2023년 5월 12일

제목 보고 왕간다님의 영상이 생각나서 들어왔네요ㅎㅎ sentry사용에 관심이 있었는데 후기를 보니까 반갑네요!

답글 달기
comment-user-thumbnail
2023년 5월 13일

테오스프린트가 끝난 이후에도 프로젝트를 계속 디벨롭 해나아가는 과정이 정말 멋있어요 :)

답글 달기