[회고] 프론트가 기획, 디자인, 배포까지 하면... (긴 글 주의)

Chaejung·2023년 10월 10일
3

🍡묘정송편?

⭐ 소원을 빌고 송편을 받자 ⭐
달나라에 사는 토끼 요정 묘정이가 특별한 송편을 만들어줍니다!
나를 위해, 혹은 다른 사람을 위해 소원을 빌어보세요🙏

이번 글은 2023년 9월 11월부터 27일까지 약 2주간 진행한 묘정송편 프로젝트에 대해
개발 사이클을 기반으로 한 회고이다.
글의 말단에서는 프로젝트에 대한 KPT 회고도 포함시켰다.

Analysis 요구사항 분석

요구사항 분석이면서 프로젝트를 시작하게된 계기는 다음과 같다.
프로지방러는 9월 초부터 추석 때 서울에서 지방으로 가는 KTX를 예매하느라 추석이 곧 다가오는 걸 알 수 있었다. 그래서 문득 이벤트성 웹사이트를 만들면 어떨까하는 생각이 들었고,
내가 어떤 문제를 해결할 수 있을지 고민을 하게됐다.

주요하게 생각했던 포인트는 다음과 같다.

  • 도메인 지식 및 데이터가 크게 필요하지 않음
  • 남은 기간(3주) 간 개발이 충분히 가능함
  • '추석' 때 사용성이 증가할 법함

이런 포인트를 고려하면서 내가 이맘때, 그리고 벌써 한 달이 지난 지금까지도 가지고 있는 문제는 '심신이 지쳤다'는 것이다. 내 생일이 있는 9월이지만서도 육체적으로, 정신적으로 좋지 않은 시기였다.
그래서 '어떤 부분이든 결핍을 가지고 있는 것을 사람들끼리 서로 위로할 수 있는 서비스'를 주된 방향으로 잡았다.

추석이라는 이벤트를 위주로 브레인스토밍을 한 결과,
올해 신묘년의 토끼🐰, 추석 전통 음식의 송편🍡이 합쳐서 '정을 나누는 토끼, 묘정이의 송편'이라는 컨셉이 구체적으로 잡혀지게 되었다.

기획

아이디어가 반짝! 떠올랐을 때는 백엔드도 내가 전부 해볼까 싶었는데, 기획도 구체적으로 나오지 않았고, 기한 내에 백엔드 서버 배포까지 하기는 어려움이 있다고 생각해서 전 직장 동료분께 SOS 요청을 했다.

그리고 흔쾌히 함께 하겠다고 하셔서 서둘러 동료로 섭외했다!
감사해요 백엔드 마스터~!

그래서 같이 기획부터 디테일하게 잡고 가면 좋을 것 같아서
만나서 유저 플로우를 짜면서 추가하고 싶은 기능과 API 기준으로 디테일을 잡아나갔다.

역시 백엔드를 많이 해본 사람의 관점으로 볼 때 집중하는 부분이 달라서 섭외하길 정말 잘했다는 생각이 들었다.
예를 들면 소원을 작성하는 사람의 이름을 unique하게 가져갈 것인가, 소원마다 password를 걸어서 비공개 처리를 할 것인가와 같은 미처 생각지도 못한 부분들을 집어주셨다!
기획도 그에 맞춰서 기능이 추가되고 처음에 러프하게 생각했던 플로우에서 구체화시킬 수 있었다.

와이어프레임 및 화면 디자인

추가하고 싶은 기능들은 계속 생겨났지만,
디자이너가 할 일도 내가 하게 돼서, 일이 두 배로 늘어나지 않기 위해 자제했다.
그래서 대략적으로 나온 화면을 와이어프레임으로 작성하면서 동시에 기능에 걸맞는 기술 스택을 조사하고 정한 뒤 초기 설정을 병행했다.

프론트엔드를 담당하면서 동시에 디자인을 하는 동안, 댜음과 같은 부분들을 유념하면서 작업했다.

  • 화면 별로 route 이름을 어떻게 갖고 갈지
  • UI 컴포넌트의 재사용성을 어떻게 높일 수 있을지

point1. 나의 작은 버튼 디자인 시스템

프로젝트 규모가 큰 편은 아니라서 고민하는 데에 있어서 많은 시간이 걸리진 않았다.
오히려 더 컸으면 디자인 시스템을 직접 구축해보는 데 있어서 좋은 경험이 될 수도 있었을텐데라는 아쉬움도 들었다.
아무쪼록 위와 같은 부분을 신경쓰면서 동시에 코드에 녹여낼 수 있도록 다음과 같이 Button.tsx라는 컴포넌트를 생성했다.

커스텀이 필요한 사이즈(너비, 높이, 폰트크기)와 색(테두리색, 버튼색)을 type과 color의 prop에 따라 결정되도록 구성했다.
다소 오버 엔지니어링으로 볼 수 있겠지만, 당시에 와이어프레임을 작성할 때도 기능 추가 및 페이지 추가가 유연하게 될 수 있도록 하기 위해 버튼 별로 따로 컴포넌트를 제작하지 않고 커스텀할 수 있도록 만들었다.

issue1. Styled Component Warning


위의 방식에서 Styled Component의 props로 임의의 값을 내려줄 때, 네이밍이 DOM attribute에 해당하지 않아서 발생하는 오류였다.

render가 안되는 것은 아니지만, console에 뜨는 log라서 그냥 지나칠 수 없었다. 그래서 warning에 나와 있는대로 shouldforwardprop을 설정해서 해결했다.

How do I provide props to styled elements in Typescript?
What is the purpose of shouldForwardProp option in styled()?

캐릭터 디자인

다시 보니 캐릭터 디자인이라고 하기에 너무 민망하군...
작업 순서로 따지자면 사실 캐릭터 디자인이 가장 나중에 완료한 것이지만,
디자인 섹션에 포함시켜서 이야기한다.

전문 일러스트레이터는 아니지만 그림 그리는 걸 좋아해서 조금씩 그려왔었는데,
그게 가끔씩 프로젝트할 때 도움이 되곤 한다.
아래 그림은 그냥 냅다 자랑하기!

이번에도 사실 조금 더 다양한 묘정이를 그리고 싶었으나 감기몸살 + 서류 지원 + 클라우드 교육 시작 + 매주 코딩테스트 콤보로 시간을 많이 내지 못했다.

TMI 1. og tag의 미리보기로 들어가는 이 이미지는 추석 전 날 집으로 가는 여정 중, 지하철에서 그렸다.(아마 1호선 군포역을 지나갈 때 쯤...)

아무튼 최종 묘정이는 [완전 꼬질/덜 꼬질/깨끗] 중에서 덜 꼬질이 팀원의 만장일치로 선정이 되었다.

끼얏호!
그래도 익숙해져서 그런지 덜 꼬질 묘정이가 가장 귀엽다!

Design 설계

기술 스택

  • React query
    소원을 보여주는 목록 페이지에서 무한 스크롤 처리와 POST 요청 유무에 따라서 페이지 분기 처리를 하는 데 있어서 빠르게 하기 위해 도입했다.

  • React spring
    10 Best Javascript Animation Libraries to Use in 2023 이 글을 읽고 나서 페이지 애니메이션을 적용하기 용이하다고 하여 도입했다.

  • Recoil
    뒤에서 Funnel 구조를 이야기할 때 한 번 언급하겠지만, 필요할 줄 알아서 미리 add 해 놓은 것을 추후 uninstall하게 되었다! 조금이나마 빌드 용량을 줄였다고 볼 수 있지 않을까?

  • Bun
    Why You Should Use Bun 이 글을 읽고 나서 빠르면 얼마나 빠르길래! 싶어서 바로 패키지 매니저로 도입했다.

point2. bun initialize

  • OS: macOS, m1
  1. Home brew bun install

    Installation | Bun Docs

    brew tap oven-sh/bun

    brew install bun

  2. App Initial

    bun create react-app . --template typescript

    보이다시피 yarn에 비해 초기 install은 그렇게 빠른지 잘 모르겠다. 내가 잘못 파악해서 그런가, 아니면 다른 쪽에서 더 빠른 건가, 이 부분은 더 공부해야할 것 같다.

  3. Add Libraries
    bun add -d eslint prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-prettier eslint-plugin-prettier eslint-plugin-import eslint-import-resolver-typescript

    • eslint: ESLint core library
    • @typescript-eslint/parser: a parser that allows ESLint to understand TypeScript code
    • @typescript-eslint/eslint-plugin: plugin with a set of recommended TypeScript rules
    • The best solution here is to use a plugin [eslint-config-prettier](https://github.com/prettier/eslint-config-prettier) to disable all ESLint rules that are irrelevant to code formatting, as Prettier is already good at it:
    • eslint-plugin-prettier: Runs Prettier as an ESLint rule and reports differences as individual ESLint issues.
    • eslint-plugin-import

    <참고 자료>
    How to use ESLint with TypeScript | Khalil Stemmler
    Linting in TypeScript using ESLint and Prettier - LogRocket Blog
    eslint-plugin-import
    ESLint 알고 쓰기
    3 Ways To Use Bun With Create React App

    다른 라이브러리를 추가적으로 install할 때는 조금 더 빠른 것 같기도 하다!

프론트 앱 배포

항상 프론트 코드만 똥땅거리다가 인프라를 처음부터 해보는 것을 처음이라... 매우 떨렸다.

하지만 AWS에서 하라는대로 하면 되겠지~라는 마음으로 차분히 따라가보았다.

휴 앱을 설정하는 것까지는 문제가 없다!

issue2. amplify + bun build

그런데 초기 설정되어 있는 빌드 명령어를 보고...
'앗, 나는 bun을 쓰는데 그러면 명령어로 bun으로 바꿔야 하는 거 아냐?'

이후 이어지는 잘못된 build 명령어와 build failed 메시지...

이건 관련된 기술블로그나 참고할 만 한 게 없어서 bun 공식문서와 amplify 빌드 문서를 읽고 이것저것 시도해보면서 답을 찾았다.

쨔란

issue2-1. amplify regions...

본격 틀린 그림 찾기! 그것은 바로바로...

예... 제가 버지니아 북부까지 갈 줄 누가 알았겠어요...
앱을 그대로 가져가서 지역만 변경할 수 있는 방법이 있나 찾아보았지만, 별 방법이 없어서, 다시 한국 region으로 앱을 재생성하고 지웠다!

Development 구현

이 단계 쯤에서 생각보다 일정이 딜레이가 돼서 동료를 한 분 더 섭외했다!

감사합니다, 프로 밤샘러 프론트~!
이 분은 묘정송편의 핵심인 달나라🌝를 전부 작업해주셨습니다. 👍

Funnel 구조

토스ㅣSLASH 23 - 퍼널: 쏟아지는 페이지 한 방에 관리하기

언젠가 toss의 기술 컨퍼런스에서 위 영상을 본 적이 있다. 항상 토스 서비스를 이용하면서 반복적으로 나오는 페이지 구조를 User 친화적으로 잘 구성했다고 생각했다.
이런 페이지 유형을 Funnel이라고 했고, Toss는 해당 페이지 구조를 재사용성, 관심사 분리 측면에서 코드 설계를 한 사례를 공유하기도 했다.
인상깊게 봤기 때문인지, 페이지 와이어프레임을 짠 뒤로 바로 Funnel이 생각났다.
찾아보니 커스텀훅으로 쓸 수 있도록 라이브러리로 만든 것 같다.
그런데 굳이 라이브러리를 쓰진 않고, 코드 구조를 벤치마킹하여 우리의 Story 레이아웃을 Funnel 구조로 구현했다.

point3. Funnel + react-spring

더 자세한 코드는 여기에 있어요.

장점

  • story에서 필요한 전역 데이터를 라이브러리를 쓰지 않고, 레이아웃 컴포넌트 한 곳에서 관리하고, api 로직까지 담을 수 있다.
  • 페이지 단계의 순서가 바뀌어도 2개의 파일에서 코드를 수정하지 않고 한 곳에서 수정해서 용이하다.
  • react-spring으로 페이지 전환 애니메이션을 구현하기가 참 쉬웠다.

단점

  • 페이지 단계가 길어질 수록 한 컴포넌트내에 코드 길이가 길어진다.
    - 페이지 네이밍을 명확하게 하지 않으면 너무 헷갈린다!

TD(M) 테스트 배포 운영

Amplify로 브랜치 별로 배포 도메인을 분리할 수 있어서 E2E 테스트를 하기에 정말 좋은 환경이었다.
그래서 develop에 merge 하면서 꾸준히 여러 기능들을 테스트 해보았는데...

issue3. 안드로이드 카카오톡 공유 이슈

안드로이드 유저: 카카오톡으로 받은 링크에서 소원을 작성했는데 링크 복사나 공유가 안돼요!

아놔, 아이폰 유저는 생각지도 못한...
그래서 추석 때 본가에 가서 어머니 휴대폰을 잠시 빌려서 테스트를 해 본 결과,
크롬에서는 잘 작동되지만, 안드로이드 + 카카오톡 인앱 브라우저의 경우 링크 복사가 안되는 것을 찾았다.

그래서 다들 카카오톡 공유하기 api를 쓰는 거였나...

이 문제는 카카오톡 인앱 브라우저 자체의 문제이기 때문에 공유하기 로직에서 이를 처리하기 보다는, 처음 앱을 접근했을 때부터 사용자의 브라우저 환경에 따라 외부 브라우저로 튕기도록 하는 것이 가장 적합하다고 생각해서 index.html을 건드렸다!

  <script type="text/javascript" charset="UTF-8">
    var inAppDeny = (callback) => {
      if (document.readyState !== "loading") {
        callback();
      } else {
        document.addEventListener("DOMContentLoaded", callback);
      }
    };
    inAppDeny(() => {
      /* Do things after DOM has fully loaded */
      async function copyToClipboard(val) {
        try {
          await navigator.clipboard.writeText(val);
          alert("URL 복사를 성공했어요.");
        } catch (err) {
          alert(
            "URL 복사를 실패했어요. 직접 복사해서 다른 브라우저가 열리면 주소창을 길게 터치한 뒤, '붙여놓기 및 이동'를 누르면 정상적으로 이용하실 수 있어요."
          );
        }
      }
      function inAppBrowserOut() {
        copyToClipboard(window.location.href);
        location.href = "x-web-search://?";
      }
      var userAgent = navigator.userAgent.toLowerCase();
      var target_url = location.href;

      if (userAgent.match(/kakaotalk/i)) {
        //카카오톡 외부브라우저로 호출
        location.href =
          "kakaotalk://web/openExternal?url=" + encodeURIComponent(target_url);
      } else if (userAgent.match(/line/i)) {
        //라인 외부브라우저로 호출
        if (target_url.indexOf("?") !== -1) {
          location.href = target_url + "&openExternalBrowser=1";
        } else {
          location.href = target_url + "?openExternalBrowser=1";
        }
      } else if (
        userAgent.match(
          /inapp|naver|snapchat|wirtschaftswoche|thunderbird|instagram|everytimeapp|whatsApp|electron|wadiz|aliapp|zumapp|iphone(.*)whale|android(.*)whale|kakaostory|band|twitter|DaumApps|DaumDevice\/mobile|FB_IAB|FB4A|FBAN|FBIOS|FBSS|trill|SamsungBrowser\/[^1]/i
        )
      ) {
        //그외 다른 인앱들
        if (userAgent.match(/iphone|ipad|ipod/i)) {
          //아이폰은 강제로 사파리를 실행할 수 없다
          //모바일대응뷰포트강제설정
          var mobile = document.createElement("meta");
          mobile.name = "viewport";
          mobile.content =
            "width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no, minimal-ui";
          document.getElementsByTagName("head")[0].appendChild(mobile);
        } else {
          //안드로이드는 Chrome이 설치되어있음으로 강제로 스킴실행한다.
          location.href =
            "intent://" +
            target_url.replace(/https?:\/\//i, "") +
            "#Intent;scheme=http;package=com.android.chrome;end";
        }
      }
    });
  </script>

카카오톡 일해라잇!!

version 1.0.0 배포!!

두근두근 역사적인 순간!

추석 전까지 불태운 서비스... 집에 가서도 쉬지 못하고 머리 쥐어싼 프로젝트...
홍보를 하면서 좋은 말씀을 많이 해주셔서, 힘들었던 것을 다 잊어버렸다.

모니터링 및 비용

예상했던 것과 달리 도메인이 가장 비쌌다! 이 도메인으로 다른 프로젝트를 또 해볼까 싶다.

KPT 회고

Keep

  • 시작을 하고나서 포기하지 않고 서비스를 출시했다. 항상 실행력만 좋지 뒷심이 부족했는데 이번에는 그 부분을 잘 극복했다.
  • 여러 가지 새로운 기술 스택을 도입해보았다. (React spring, bun, amplify)
  • CI/CD를 잘 정립하여 배포 후 에러를 잘 처리했다.

Problem

  • 프론트끼리 코드 리뷰를 하지 못하고 급하게 merge하는 데에 급급했어서 아쉽다. 뒤늦게 합류하신 상민님께 제대로 얼라인을 맞추는 시간을 갖지 못해 적응하는 데에 도움을 드리지 못했다.
  • 기획 자체는 무난하지만, 주 타겟층을 세분화하지 못해 결국 서비스 리텐션을 높이지 못했다.
  • 애니메이션을 좀 더 다양하게 주고자 했는데 시간이 부족해서 그러지 못했다.
  • 모바일 특화 인터렉션에 신경쓰지 못했다(ex. 뒤로가기 없음, 처음으로 가기 확인하기 어려움, 스크롤 위치 초기화)

Try

  • 개인적인 바람으로는 이 구성대로 명절 또는 시즌 별 새로운 프로젝트를 한 번 더 진행하고 싶다.이번 프로젝트가 체계적인 편은 아니었으나 그럼에도 불구하고 각자 주도적으로 할 일을 맡아서 해주었기에 성공했다고 생각하기 때문. 따라서 좀 더 체계적이고 기간을 두어 작업을 한다면 좀 더 나은 결과를 내놓을 수 있을 거라 생각
  • 디자이너를 섭외해 프론트는 프론트의 업무, 즉 애니메이션 효과/화면 구성을 기술적으로 구현하는 데에 집중하도록 한다. 디자이너에게 기획 의도를 잘 드러날 수 있도록 UI를 구성하거나 페이지를 만드는 작업을 맡긴다.
  • 피쳐 개발 또는 다른 프로젝트가 아니더라도 현재 프로젝트에 대한 코드 리뷰를 진행한다.(FE, BE 불문)

TMI

  • 폰트는 내 글씨체다!
    여기서 체험할 수 있어요!

  • 구현 중에 토스가 유사한 컨셉으로 먼저 이벤트를 출시했다..

    그...그치만 우리 묘정이 더 귀여워!!!!

  • 다음은 더 강해지고 귀여워진 묘정이로 찾아오겠습니다!
profile
프론트엔드 기술 학습 및 공유를 활발하게 하기 위해 노력합니다.

0개의 댓글

관련 채용 정보