/HabitMonster-FrontEnd

몬스터가 성장하는 만큼, 당신의 습관도 성장할 거에요. 몬스터와 함께하는 습관 형성 서비스

Primary LanguageJavaScript

Habit Monster Netlify Status

image

1. 프로젝트 소개

몬스터가 성장하는 만큼, 당신도 성장할 거에요. 몬스터와 함께하는 습관 형성 서비스

우리들의 삶은 습관으로 이루어져 있다고 생각합니다.
오늘 하루 우리는 습관대로 생각하고, 말하고 행동해왔을 겁니다.
이렇게 습관은 우리의 정체성을 결정하며 삶의 방향에 영향을 미칩니다.
저희 팀은 사용자들이 더 재미있고 즐겁게 좋은 습관을 만들어가는 서비스를 만들고 싶었습니다.
가장 친숙하게 다가 갈 수 있는 다마고치와 같은 게임에서 영감을 얻어. 몬스터를 키워가며 습관을 성장시키는 서비스를 제작했습니다.


2. Team Members

Back-end (Spring)

  • 이정인, 강준규, 최원빈 (Team Leader)

Frontend (React)

  • 오세명, 윤상준, 배재경

Design

  • 김남희, 김도희, 김소영

3. 기술스택

  • React
    • Styled-component
    • react-device-detector
    • react-router-dom
    • swiper
    • react-error-boundary
    • Universal-cookie
  • Recoil
  • axios
  • workbox modules

3.1 기술 스택 선정 이유

  1. React
  2. Recoil(V0.5.2)
    • 부트캠프 과정을 수료하는 동안 전역 상태(이하 글로벌 스테이트)를 효율적으로 관리하기 위하여 Redux 라이브러리를 학습했습니다. Flux패턴을 배우면서 예측 가능한 데이터 플로우를 그릴 수 있다는 장점과 보일러플레이트, 높은 러닝커브와 같은 단점을 알게 되었습니다.
    • 프로젝트 규모가 작은 상황 속에서 Redux를 배웠으니 Redux를 써야겠다는 수동적인 생각보다는 단순하게 글로벌 스테이트를 표현할 수 있는 패키지를 도입하여 코드 볼륨을 줄여보고자 하는 목표를 가지게 되었습니다. 관련 리서치를 통해 후보군 2개(React Query, Recoil)를 만들었으며, 저희는 Recoil을 도입하기로 하였습니다.
    • Recoil은 Atom과 Selector라는 Unit으로 스테이트를 표현하며, 사용 인터페이스는 리액트 훅과 거의 동일하다는 강력한 장점이 있습니다. 저희는 이러한 간편함과 동시에 Dependency만을 활용하여 비동기 업데이트를 수행할 수 있다는 점에서 많은 매력을 느꼈습니다.
    • 그러나 정식으로 출시된 패키지가 아니어서 안정성의 우려가 있었습니다. 저희는 리코일 공식 Github Issue를 위주로 사용성에 문제가 없는지 검토 하였으며, 프로젝트에 필요한 기능을 구현하기에는 안정적이라고 판단하였습니다. 그 결과 프로덕션 환경에서도 특별한 이슈 없이 기능을 구현할 수 있었습니다.
  3. React-router-dom(V5)
    • UI를 url에 따라 분기 처리하여 렌더링하기 위해서 선택하였습니다.
  4. React-device-detector
    • Mobile-Friendly한 웹앱을 만드는 것이 프로젝트의 주안점이었지만 웹사이트로 접속하였을 때 랜딩 페이지의 뷰가 따로 그려졌으면 좋겠다고 판단하였습니다. 이를 위해 유저 에이전트 별로 다른 뷰를 분기 처리하여 렌더링 할 필요가 있었습니다. 유저가 어떤 프로그램을 통하여 프로덕트에 접근하였는지 감지하기 위해 해당 패키지를 선택하였습니다.
  5. React-error-boundary
    • 런타임 에러가 발생할 때에도 서비스 플로우를 유지하고자 도입하였습니다.
  6. Universal-cookie
    • JWT 토큰을 쿠키에 안전하게 보관하기 위해 도입하였습니다.
  7. Swiper
    • 모바일 환경에서의 터치 이벤트(스와이프에)에 잘 반응하는 Slider를 표현하기 위해 선택하였습니다.
  8. Styled-components
    • PostCss와 Styled-components를 고민하던 도중, 디자이너와의 원만한 협업을 하기 위해서는 익숙한 라이브러리를 선택하는 것이 좋다고 생각하여서 선택하였습니다.
    • styled-reset 패키지를 추가로 설치하여 유저 에이전트의 기본 CSS 설정을 초기화하였습니다.
  9. Axios
    • Interceptor를 통하여 서비스 흐름 중 유저 인증 플로우를 효과적으로 그리기 위해 선택하였습니다.
  10. workbox modules
    • 정적 자산을 프리캐싱하여 모바일에서 빠른 속도로 자산을 불러오기 위해 선택하였습니다.

4. 작업시 준수했던 사항

  1. 스크럼

    • 매일 오전 11시, 간단한 미팅을 통해 각자의 작업 상황과 특이사항을 공유하였습니다. 특이사항은 문제 해결 과정 속에서 겪었던 이슈와 도움이 필요한지 여부를 작성했습니다. 그 후, 상대적으로 Task를 빨리 끝낸 사람과 함께 최대한 빠르게 해결하였습니다. 지속적인 소통 결과, 당초의 예상보다 앞당겨 프로덕트를 배포할 수 있었습니다.
  2. 칸반

    • 협업을 하다보면 서로의 Task가 선후관계로 엮이는 상황을 마주칠 수 있습니다. 이를 사전에 인지하고 풀어나가기 위해서 각각의 Task를 시각화 할 필요가 있었습니다. 프로젝트의 3주차부터 Github Issue와 노션 페이지를 활용하여 칸반을 작성하였습니다.
    • 디자이너와 칸반을 공유하여 뷰 관련 업무 분담 역시 진행하였습니다. 프론트엔드 칸반 디자이너/프론트엔드 업무 분담표
  3. CSS

    • 세 명의 팀원 모두 CSS를 작성하는 스타일이 달라서 컨벤션을 정하고 스타일을 통일시킬 필요가 있었습니다. 회의 끝에 Styled-components의 createGlobalStyle 내부에서 글로벌 변수를 선언하도록 하였습니다.
      • Figma 디자인 시안 중 글자 크기, 색깔과 같은 공통적인 요소부터 작업 도중 공유할 필요가 있는 CSS 프로퍼티까지 글로벌 변수로 선언하였습니다.
    • 다수의 공통된 프로퍼티를 공유하고자 할 때에는 Mixin을 사용하였습니다.
  4. 프론트엔드 개발자로서의 기본 소양 중시

    • 프로젝트 시작 전, 프론트엔드 개발자를 지망하는 이유에 대해 서로 이야기했습니다. 이유는 다양했지만 서비스의 최전선에서 유저와 소통하는 것이 즐겁다는 공통점을 가졌음을 파악했습니다.
    • 또한, 프론트엔드 주니어 개발자가 되기 위해서 무엇이 필요할까 함께 고민을 하였으며, 값을 다루는 논리적인 사고력 뿐만 아니라 UI 구현 역량도 중요하다는 점에서 서로 공감했습니다.
    • 따라서 저희는 디자이너와 소통하며 UI와 UX를 정확하게 구현하되, 무분별한 라이브러리 도입을 지양하고 최대한 직접 구현하자는 원칙을 세웠습니다. 그 결과 터치 이벤트에 정교하게 대응할 필요가 있는 컴포넌트를 제외한 나머지 컴포넌트를 직접 설계하고 구현할 수 있었습니다.
    • 유저 피드백 중 UI가 깔끔하고 정교하다는 내용을 보았을 때 보람을 느꼈습니다.

5. 주요 기능 구현 과정

장기간 동안 지치지 않고 작업을 할 수 있었던 이유는 그 안에서 배움의 순간을 즐겼기 때문입니다. 본 장에서는 비즈니스 로직을 구현하면서 익혔던 기술과, 해당 기술을 적용할 때 겪었던 문제점, 그리고 해결 과정을 다룹니다.


5.1 상태 관리

5.1.1 무엇을 글로벌 스테이트로 두는가?

개발 초기 단계에서는 백엔드로부터 받은 모든 응답을 로컬 스테이트로 관리하였습니다. 그 후 각 응답에 대해 전역 관리 필요성 여부를 점검하였습니다. 여러 페이지에서 공유하는 응답은 글로벌 스테이트로, 단일 페이지에서 사용되는 응답은 로컬 스테이트로 관리하기로 결정하였습니다.

또한 비즈니스 로직에만 국한되지 않고, 단순히 UI의 변화에만 쓰이더라도 여러 컴포넌트에서 공유하는 값은 모두 글로벌 스테이트로 관리하였습니다.

5.1.2 무엇을 아톰과 셀렉터로 두는가?

리코일의 목적을 최대한 따르기 위해 먼저 각 값이 지니는 성격을 구별했습니다.

  • 유저가 등록한 습관은 다른 외부 요인에 의해 변화될 가능성이 없으므로 API Call이 유효하다면 setState를 통해 클라이언트에서 변경해도 성질이 유효합니다.
  • 팔로워의 경우 한 번 값을 받은 시점부터 그 값이 현실을 반영하는지 알 수 없습니다.
  • 이처럼 값의 시간적인 성질을 파악하여 Stale해도 괜찮다면 아톰으로, 실시간 갱신이 필요하다면 셀렉터로 관리하였습니다.

state

5.1.3 Problems and Solutions

  1. 하나의 클라이언트에서 유저가 변경된다면?
  • QA 과정에서, 로그아웃 또는 탈퇴 이후 다른 계정으로 로그인을 하면 이전 계정의 습관이 보이는 버그를 발견하였습니다. 이는 컴포넌트가 아닌 프로그램의 라이프 사이클을 따르는 아톰의 성질이 원인이었습니다.
  • 로직을 설계할 당시, 최초 로그인을 제외한 다른 모든 로그인 이벤트가 발생하면 습관 아톰의 기본 값인 셀렉터를 다시 평가 하도록 설계했습니다. 하지만 그 셀렉터를 구독하는 컴포넌트가 없기 때문에 업데이트가 이루어지지 않는다는 것을 발견하였습니다.
  • 이는 useRefresher 커스텀 훅을 제작하여 해결했습니다. 먼저 해당 셀렉터에서 캐싱된 값을 초기화 한 후, useRecoilCallback으로 업데이트 된 셀렉터에 직접 접근하여 아톰을 업데이트하였습니다.
  • 어떤 라이브러리를 도입할 때에는 그 라이브러리가 가진 철학을 잘 이해해야 효과적으로 사용할 수 있다는 것을 깨달았습니다. 또한 로그인처럼 반복될 수 있는 이벤트는 클린업이 중요하다는 것을 알게 되었습니다.

5.2 클라이언트 라우팅

5.2.1 서비스 플로우를 효과적으로 유지하려면?

유저가 URL을 직접 입력하여 접속할 경우를 대응할 필요가 있었습니다. 이를 위해 페이지 라우팅의 시발점이 되는 컴포넌트에서 관련 값을 체크하여 분기 처리하였습니다. 비즈니스 로직은 다음과 같습니다.

  1. 로그인하지 않았을 때
    • 로그인 페이지로 리다이렉팅.
  2. 로그인했지만 캐릭터를 고르지 않았을 때
    • 몬스터 선택 페이지로 리다이렉팅.
  3. 로그인했을 때
    • 몬스터 레벨이 5가 되지 않았는데 캐릭터 선택 페이지로 직접 접속했을 때
      • 메인 페이지로 리다이렉팅.
    • 그 외의 페이지로 직접 접속했을 때
      • URL에 따라 리다이렉팅 또는 정상적으로 렌더링.

5.2.2 Problems and Solutions

  1. 다수의 페이지에서 분기 처리할 경우 복잡도 증가
  • 2번 케이스의 대응 과정에서 경우의 수가 급격히 증가한다는 문제점을 발견했고 유지 보수를 위해 단순화할 필요가 있었습니다.
  • 원인은 몬스터 선택 플로우에 필요한 뷰를 전부 페이지로 관리하였기 때문입니다. 총 3개의 뷰를 순차적으로 보여주어야 하는데, 페이지로 관리를 하다 보니 "해당 페이지에 접근 가능한 시점"에 대한 경우의 수를 전부 고려해야 했습니다.
  • 이를 해결하기 위해 3개 페이지를 1개 페이지(/select) 안에 3개 step으로 통합했습니다. 합쳐진 페이지 내부에서는 다음 step으로 넘어가는 기능의 로컬 스테이트를 설정했습니다. 각 단계에서 버튼 클릭 이벤트가 트리거 될 때마다 setState가 호출되어 다음 단계로 이동하도록 로직을 정비하였습니다.
  • 해당 이슈를 접한 후 모든 뷰를 페이지화하여 관리하는 것이 항상 최선은 아니라는 점을 깨달았습니다. "이 뷰가 페이지로 들어가도 괜찮을지?"에 대해서 고민할 수 있어서 즐거웠습니다.
  1. 뒤로가기 오작동
  • QA 과정 중 뒤로가기 버튼을 눌렀을 때 팔로우/팔로잉 탭과 같은 NavLink에 바인딩 된 path를 건너 뛰고 그 전의 페이지로 이동해야 할 필요성이 생겼습니다.
  • 문제의 원인은 뒤로가기를 하였을 때 단순히 history.goBack()을 호출하여 히스토리 스택 이전으로 이동하는 것이었습니다.
  • 문제를 해결하기 위해 path를 직접 스택 자료구조의 형식으로 보관했고, 라우팅 시 스테이트로 전달하였습니다. 뒤로 가기 이벤트를 트리거하면 location.state에 저장된 스테이트를 pop하여 해당 경로로 replace 하여, 페이지로 라우팅을 시킬 수 있었습니다.
// before
<BackButtonHeader
  onButtonClick={ () => history.goBack() }
/>

// after
<BackButtonHeader
  onButtonClick={() => {
    const copyStack = location.state?.prev.slice();
    const path = copyStack.pop();
    history.replace(path, { prev: copyStack });
  }}
/>
  • 관련 문제를 react-router-dom에서 제공하는 기능에만 의존하여 해결하려고 했습니다. 라이브러리에 종속된 사고 방식을 뛰어 넘어 라이브러리를 저희가 작성한 로직과 융합시켜 활용할 줄 알아야겠다고 느꼈습니다.

5.3 PWA 적용 및 배포

5.3.1 모바일 친화적인 사용자 경험을 제공하려면

프로젝트 형태와 주요 기능이 웹사이트가 아닌 웹앱의 구조에 가까웠기 때문에 웹사이트보다는 모바일에서의 사용성을 우선적으로 고려하였습니다. 지금까지 익혔던 웹 기술만 사용해서는 네이티브 앱처럼 동작하게 만드는 것이 어려울 것이라 판단하여 PWA를 적용하였습니다.

PWA는 manifest.json 을 통해 웹앱의 구성 및 메타정보를 설정할 수 있습니다. manifest의 display 속성을 standalone 옵션으로 지정하여 주소 표시줄을 숨기고, 새 창에서 실행 및 전체 화면으로의 전환 등의 작업을 가능하게 하고 모바일 앱과 유사한 화면에서 동작할 수 있도록 설정 했습니다.

그리고 service worker를 등록하여 브라우저의 백그라운드에서 service worker가 설치되는 동안 웹앱이 갖고 있는 정적 자산을 캐싱하게 했습니다. 해당 자원 요청시 보유하고 있는 정적 자산을 제공해주어 불필요한 요청을 줄이고 네트워크 속도나 환경에 구애받지 않는 실행 성능을 보장할 수 있었습니다.

이후 QA를 통하여 지속적으로 모바일 환경에 배포본을 설치하여 테스팅을 진행하였습니다.

5.3.2 Problems and Solutions

  1. 캐싱된 자원을 갱신하지 못하는 문제
  • QA를 할 때 Precache Manifest가 배포 후에도 직전 버전을 반영하고 있어 업데이트된 컨텐츠를 보여주지 못하는 문제가 발생하였습니다.
  • 해당 문제를 해결하기 위해서 배포할 때 마다 다르게 설정하는 환경변수를 이용하여 workbox-core 패키지의 setCacheNameDetails으로 새로운 서비스워커의 cacheName을 설정하고, beforeunload 이벤트가 트리거될 때 skipWaiting 메서드를 호출하였습니다.
  • 이 문제를 인지하였을 때, 시간이 촉박하여 제대로 알아보지 못하고 레퍼런스가 지금 현재 프로젝트 상황에 적합한지 판단 후 해당 레퍼런스를 그대로 참고하여 아쉬움이 많이 남습니다. 리팩토링을 하면서 보완하겠습니다.
  1. 모바일로 배포본을 실제로 확인하였을 때 겪었던 문제들
  • 모바일 웹사이트에서 배포본을 테스트하였을 때 화면이 휴대폰 상하단 탭을 초과하는 문제를 발견하였습니다.
    • 모든 모바일을 기기의 높이를 고려하고자 기본 height 값을 100vh으로 설정하였는데, 모바일에서는 100vh이 실제 document.innerHeight보다 크게 측정되는 것이 원인이었습니다.
    • 해당 문제를 해결하기 위해서 최초 렌더링이 될 때 document의 스타일에 글로벌 변수 —vh 를 innerHeight의 1%으로 지정하고, html, body 태그에 해당 변수를 height 프로퍼티에 지정하였습니다.
    • 재배포 후 다시 테스팅을 하였을 때 리사이즈 이벤트에 대응하지 못하여 해당 이벤트 핸들러를 추가적으로 달아 해결하였습니다.
  • 다양한 디바이스 환경에서 보이는 뷰의 차이
    • 저희는 default width를 360px으로 정해 작업을 진행하였으며, 디자인 시안이 고정 픽셀로 제시되어서 막연하게 해당 픽셀을 그대로 반영하면 된다는 생각을 가졌었습니다.
    • 그러나 실제 모바일 뷰에서 테스팅을 하였을 때 기대했던 것보다 더 짧거나, 더 작아보이는 문제에 직면하였습니다.
    • 이 문제의 원인은 당연하게도 다양한 디바이스 환경을 고려하지 않았기 때문이었습니다.
    • 해당 문제를 해결하기 위하여 전체 폭을 차지해야하는 경우 고정 값에서 100%으로 전면 수정하였으며, 디바이스 별로 다르게 보이는 문제를 개선하기 위하여 오류를 소통 채널로 통해 스크린샷으로 바로 접수받으면 그 원인을 분석하여 실시간으로 해결 방안을 반영하였습니다.
  • 지금까지 웹개발을 공부하였지만 모바일 웹 환경에 대해서는 많이 몰랐던 저희 자신을 되돌아보게 되었습니다. 더 많은 고객을 위해서는 더 다양한 환경에 대해 공부할 필요성을 깨달았습니다.

5.4 로그인 로직 구현

5.4.1 OAuth2 소셜 로그인

사용자 친화적인 서비스 제공을 위해 가장 활발하게 사용되고 있는 구글, 카카오, 네이버 소셜 로그인을 구현했습니다. 구글과 네이버는 각각에서 제공하는 SDK를 사용했고, 카카오는 REST API 방식을 사용했습니다.

5.4.2 Problems and Solutions

  1. 구글 로그인 후 재로그인이 되지 않는 문제
  • 구글 로그인 => 로그아웃 => 다시 구글 로그인 버튼을 누를 경우 아무 반응이 없는 문제가 있었습니다.

  • window.location.href를 통해 아예 새로고침하여 이동할 경우 정상적으로 동작했었습니다.

  • 문제의 원인은 처음에 구현했던 로직에서는 구글 SDK 스크립트를 한번만 사용할 수 있다는 점이었습니다.

    • 리서치를 통해 얻은 레퍼런스로 구현했던 첫 로직은 큰 결함이 있었습니다.

      • (function (d, s, id) {
        let js;
        const fjs = d.getElementsByTagName(s)[0];
        
        // 원인
        if (d.getElementById(id)) {
        return;
        }
        ...
      • 스크립트가 이미 존재한다면 강제로 종료시켜 버렸기 때문에, 다시 실행되지 않았고 결과적으로 로그인 버튼을 눌러도 아무 일이 일어나지 않았습니다.

  • 기존에 선언된 스크립트가 있다면 삭제한 후 새로 선언하는 방식으로 수정하여 해결하였습니다.

    • (function () {
        if (document.getElementById('google-js')) {
          document.getElementById('google-js').remove();
        }
      
        const firstScriptTag = document.getElementsByTagName('script')[0];
        const scriptTag = document.createElement('script');
      
        scriptTag.id = 'google-js';
        scriptTag.src = 'https://apis.google.com/js/platform.js';
        scriptTag.onload = window.onGoogleScriptLoad;
      
        firstScriptTag.parentNode.insertBefore(scriptTag, firstScriptTag);
      })();
  • 리서치를 통해 얻은 레퍼런스를 복사 붙여넣기하던 제 자신을 반성할 수 있었고, 이러한 레퍼런스를 프로젝트에 적용하기 위해서는 충분한 검증과 고민이 필요하다는 것을 배울 수 있었습니다.


6. 추후 업데이트 사항

  • 주요 기능 구현 과정: 공용 컴포넌트 제작
  • 주요 기능 구현 과정: 정적 자산 관리
  • 프로젝트 소감