[넘블] 럽쇼츠(LuvShort) 리액트 프로젝트 회고록

Urther·2022년 5월 17일
4

프로젝트들

목록 보기
3/4

럽쇼츠 프로젝트 주소

✔️ 프론트엔드 관련 기술이 담겨있는 회고록입니다.
✔️ 프로젝트는 [크롬 개발자 도구] - [모바일] - [Samsung Galaxy S8+] 에 모바일에 최적화 되어있습니다.

프로젝트 미리 보기

자세한 동작 구현 노션 보기 👀

맞는 데이터를 채워 넣기 위해 친구 + 내 사진으로 마구 마구 왕창 넣었다.. ✨ 그래서 조금 부끄럽다.

⚒ Tech Stack

✔️ 프론트엔드

  • React JS
  • Javascript
  • Redux
  • Styled-component

✔️ 백엔드

  • Spring boot 2.6.6
  • JPA
  • mysql
  • AWS EC2, RDS, S3

👩‍🎓 배운점

좋아요 toggle

여러 번 연타로 누르면 서버에 많은 요청이 갈 수 있을 것 같아서 debounce 방식을 도입했다. 쓰로틀링 대신 디바운스를 이용한 이유는 아래 이슈에 올려놨습니다.

구현은 lodash 라이브러리를 썼습니다. 확실히 setTimeout으로 구현할 때보다 코드가 깔끔하고 가독성이 좋게 느껴졌습니다.

export const toggleLiked = debounce(
  async ({ target, heartState, setModal, setHeartState, email }) => {
    try {
      const id = target.dataset.id;

      if (heartState) {
        // 트루면 삭제
        await request(`/api/hearts/${id}`, "put", {
          userEmail: email,
        });
      } else {
        await request(`/api/hearts/${id}`, "post", {
          userEmail: email,
        });
      }
    } 
    catch (e) {
      console.log(e);
    }
  },
  200
);

여러 곳에서 사용하기 때문에 함수로 빼기 위해 생각보다 매개변수로 전달해주는 영역이 많아 복잡해보이긴 합니다 🥲

네비게이션

네비게이션 문제로 일주일을 허비했다.

현재 어디 위치에 있는지 focus 형식으로 되어야 했기 때문이다.

✔️ 문제점 1 ) 상태관리

초기 useState("main")으로 관리했는데 My 페이지로 이동시 컴포넌트가 재랜더링되며 main -> MY 로 변경되는 것이 눈에 너무 잘보였다. 해결안으로 redux로 관리하고자했는데 깜빡임 문제가 발생했다.

✔️ 해결안 - 상태관리

  • redux persist를 사용하기로 했다.

persist를 도입하면서 고민해야하는 부분은 당연히 리덕스의 모든 데이터를 로컬스토리지로 관리할 것이냐이다.

const persistConfig = {
  key: "root",
  storage,
};

이 문제에 대해서 store.js "파일에서 관리가 필요하지 않은 데이터는 blacklist로 관리하여 메모리 부담을 줄이자"였다. 결국은 모두 persist로 관리해야하는 데이터라 로컬에 넣어놨다.

persist의 도입 후 이런 문제들을 해결할 수 있었다.
그러나 추후 빌드하면서 PersistGate가 말썽을 일으켰다. ⏤ 죽어도 해결 못할 것 같다고 생각했는데 어느새 (?) 해결이 되어있었다 ⏤ persist의 document 가 잘되어 있지 않아서 원인을 알 수 없어서 많이 헤맸다.

✔️ 문제점 2 ) focus 아이콘

초기에는 네비게이션 컴포넌트에서 pathname 으로 판단했다.

그런데 뒤로가기 기능이 있어서 해당 pathname이 아니어도 focus된 네비게이션 요소를 유지해야했다. 따라서 pathname 으로 판단할 수 없었다.

✔️ 해결안 - focus 아이콘

navigation에 존재하는 (홈, 메시지, 관심영상, 알림, MY) 컴포넌트가 랜더될 때 해당 아이콘이 focus 되도록 dispatch 시켜줬다.

지금 해보라면 쉽게 할 수 있을 것 같은데 리덕스로 관리해야하는지, useState로 관리해야할지 혼돈이 있었던 프로젝트 초기에는 이렇게도 해보고 저렇게도 해보며 많이 헤맸다.

무한스크롤

메인 페이지의 무한 스크롤을 적용해야하는데 기존 무한 스크롤보다 까다로웠다. 상단의 카테고리가 변경될 때마다 그 카테고리+ 왼쪽의 카테고리에 해당하는 데이터를 랜더해줘야했다.

예를 들어 왼쪽의 interest=쇼핑, 게임, 반려동물 이고 오른쪽 cateogry=여성 이면, 업로더의 정보는 여성이고 interest와 일치하는 video 를 요청해야했다.

전체 받아온 것 중에 filter 기능을 사용해서 걸러서 video를 전달해주면 해당 카테고리에서 무한스크롤이 불가능하단 판단이 들었다. (현재 받아온 데이터에서 filter하는 역할만 담당한다고 생각했다)

✔️ 생각한 해결안

IntersectionObserver 방법을 사용했습니다.

  • 카테고리가 변경될 때 마다 초기 데이터 4개를 요청한다. (초기 lastId=100000000 으로 설정, 받아온 데이터에서 lastId 해당하는 마지막 비디오 데이터 idx로 변경해준다. )
  • target에 닿았는데 lastId가 100000000이 아니라면 데이터를 요청한다.

글로 적다보니 어렵게 느껴진다. 그니까 요청하는 부분을 두 개로 나눴다고 생각하면 된다. 카테고리 변경하면 초기 데이터를 요청하고, 밑으로 내리면 추가 데이터를 요청하는 방식으로 ! 복잡하게 느껴지지만 각각 전송해야하는 payload와 qs가 달라졌기 때문에 이게 최선이었다. ⏤ 같은 팀은 아니었지만.. ✨ @효식님의 조언으로 이러한 로직을 성공시킬 수 있었다.

videoItem의 lastIdx를 이용하여 마지막 비디오가 나오면 데이터를 요청하는 방법도 있었지만 videoItem을 컴포넌트로 분리해두어 ref 값을 가져오는 일이 귀찮아서 하단 div을 임시로 두고 target으로 지정했다. Intersection observer의 방식은 너무 많이 나와있기 때문에 그에 대한 설명은 뛰어 넘고자 한다.

axios의 request문

비동기 통신 라이브러리로 axios 를 선택했다.

axios.get("http://13.209.236.146:8080/api/videos/filter", {
  params: { id: "alswlkku@gmail.com" },
});

기본적으로 이런 방식으로 사용하고 싶은데 baseUrl이 되는 http://13.209.236.146:8080(예제) 주소를 매번 쓰는 것에 대한 피로감을 느꼈다...!

그래서 request.js 파일을 파고 나름대로 편하게 매개변수로만 전해줄 수 있었다. 이 파일을 작성하면서 axios 객체에 대해 뜯어볼 수 있었다. ⏤request 파일을 내가 작성했기 때문에 나는 편하게 사용했지만 다른 사람들이 사용하려면 request 파일을 읽어보고 이해해야한다. 이 방법이 정말 옳은 방법인지는 여전히 고민인 부분이다.

주소 뒤에 붙는 ? 뒤부터는 query string 으로 params로 보내면 된다.

{
    "categories" : ["여행","게임","요리"],
    "gender" : null,
    "city" : null,
    "district" : null
}

그 외의 body 에 있는 data 는 payload로 data 에 담아서 보내주면 된다. 백엔드 분들이 사전에 알려주셔서 qs와 payload로 보낼 것을 구분해서 요청보낼 수 있었다.

로그아웃 기능

로그인 기능을 내가 담당하지 않았지만 로그아웃 기능은 내가 맡게 되었다 !
Kakao Developers - 카카오 로그인 RestAPI 문서가 너무 잘되어 있지만 헤맸다. 프론트엔드 팀의 @성운님의 도움 덕분에 덜 헤매고 기능을 구현할 수 있었다.

처음에는 axios.get 요청으로 했었는데 로그아웃 요청을 수행하지 못하는 문제가 발생했다. 버튼을 누르면 요청을 보내는 것이 아닌 a 태그를 이용하여 저 링크로 이동하게끔 하니까 되었다... ! ⏤ a 태그로 페이지 이동을 시켜 요청을 보내나 버튼을 눌러서 요청을 보내나 일맥상통 아닌가.. ?!

그리고 로그아웃리다이렉트 페이지를 만들어 이곳에서 쿠키에 존재하던 accesstoken 을 빈문자열로 리턴시키고, localStorage 에 존재하던 정보 (persist 등)을 없애버렸다. 그리고 로그인 페이지로 리다이렉트를 시켜버렸다. 🥲🙏

당연한 얘기지만 (나에겐 .. 당연하지 않았던)

<Route path="/oauth/callback/kakao" element={<KakaoRedirectPage />} />

카카오 디벨로퍼스에 설정해둔 로그아웃 리다이렉트 URI에 맞게 route 처리를 해주면 끝이다. 처음에 리다이렉트 URI 가 왜 필요한지 몰랐는데 .. 또 하나 배웠다😮

프론트엔드의 배포 자동화

배포화 자동화 도구는 netlify 를 사용해서 git repository와 연결 시켰다.
aws를 이용하는 방법도 있었지만 커스텀 도메인 설정이 복잡하게 느껴졌다. 그래서 커스텀 도메인 설정 가능 + git과 연결해서 즉각적으로 사용할 수 있는 netlify를 사용했다.

사실, 개발할 당시에 바로 연결하지 못했다. 여러 오류들 때문이었다. 그래서 프로젝트를 마무리 할 때 git 과 연결할 수 있었다.

1. 빌드 명령어

일단 리액트를 연결해서 배포하면 404 에러가 뜬다. 그래서 deploy 명령어를 보니까 'CI false ' 이런 에러가 발생한 것을 확인할 수 있었다. npm run build 라고 설정한 빌드 명령어가 문제였다. CI= npm run build ⏤ 띄어쓰기도 중요하다.. 이걸 붙여 써서 1시간동안 삽질했다.

2. .env 파일의 환경변수는 ?

처음에 .env 파일 때문에 오류가 났다. 당연히 그것도 없이 빌드하려니 오류가 발생한 것이였다. 그래서 바보처럼 레포지토리에 .env 파일을 업로드하고 임시로 해결해놨었다.

[Build & deploy] - [Environment] 로 오면 .env 파일을 업로드 하지 않고서도 직접 환경 변수를 넣을 수 있었다. 😂

3. react-router 404 오류

single page 이므로 서버는 'public/index.html' 만 보내주게 된다. 그래서 우리는 react-router-domRoute, Routes 를 사용하여 경로를 지정해주게 된다. 이런 라우팅 처리는 결국 브라우저가 해주기 때문에 서버에서는 "난 그런거 모르는대..? " 라고 나오는 것이다.

public 폴더의 root 위치에서 _redirects 파일을 생성하고 아래와 같은 메시지를 내어주면 잘 작동한다 !

/* /index.html 200

결론


해당 브랜치에 pr을 날리거나 push 를 하면 바로 deploy 에서 새로 build 를 해주어 배포해준다.. 해결하고 나니까 너무 편한 기능이었습니다.

백엔드와의 협업

그 전에는 notion으로 백엔드 분들이 작성해주신 API 명세를 확인했다. 근데 예상치 못하게 API의 변경이 필요했고 그 때마다 API 명세를 바꿔주셔야했기 때문에 번거로웠다.

백엔드분들이 초대해주셔서 Postman이라는 유용한 도구를 배울 수 있었다. 사실 배웠다기 보다는 백엔드 분들이 작성해주신 걸 보고 query string으로 보낼지 payload로 보낼지 알게 되었다 정도다.

결론은 기술 최고다.

React-helmet

리액트 헬맷을 사용하여 홈페이지의 메타 데이터를 추가하였다.
리액트를 사용하면 페이지 컴포넌트 별로 head 값을 지정하기 어렵다. 그래서 리액트 헬멧을 사용하게 되었다.

중요한 이유

  • 검색크롤러가 public/index.html 파일만을 읽기 때문에 SEO 관점에서 좋지 않다.
  • 트위터 / 페이스북 등의 sns 공유시 페이지에 대한 설명을 미리 볼 수 있다.

😮 리액트 헬맷은 어떻게 적용되는 것일까?

reference | 김정환 블로그 - react-helmet의 동작 방식

  • 리액트는 body 요소만 변경해주는 것인데 어떻게 head 값을 변경해주는 것일까?
    • 리액트 헬맷은 DOM-API를 사용하여 직접 헤더를 변경해주는 방식이다. ⏤ 변경해주기 위해 바닐라 자바스크립트에서 타이틀을 변경해주던 것 처럼 document.title을 사용해준다. ⏤
const updateTitle = (title, attributes) => {
  // DOM 객체의  title 속성을 직접 변경한다(이해를 위해 코드를 라이브러리 코드를 변형함).
  document.title = title
}

useEffect 훅을 사용하여 title을 변경하는 방법도 있었지만 useEffect 훅 내에서 매번 document.title="뭐뭐뭐" 이런식으로 title과 meta 태그에 따른 content를 변경하는 코드 작성이 귀찮았다. 그래서 편리하게 라이브러리를 사용했다.

👥 사용

import Helmet from "react-helmet"

.. 생략
     <Helmet>
       <meta name="description" content={description} />
       <meta name="keywords" content={keywords} />
       <title>{title}</title>
       <meta property="og:title" content={title} />
       <meta property="og:image" content="/assets/deploy.jpeg" />
       <meta property="og:site_name" content="럽쇼츠" />
       <meta property="og:description" content={description} />

       <meta name="twitter:title" content={title} />
       <meta name="twitter:description" content={description} />
       <meta name="twitter:image" content="/assets/deploy.jpeg" />
       <meta name="twitter:card" content="summary" />
     </Helmet>

초기에는 react-helmet에서 Helmet을 끌어와 썼었다. 그런데 오류가 발생하여 react-helmet-async에서 끌어와서 사용했다. 결과물은 아래와 같다.

각 페이지별로 설정을 해주어야 하는데 시간이 없어서 그런 작업들이 이루어지지 못해서 아쉬운 점으로 남는다.

react-helmet은 side-effect 문제가 존재하니 이를 방지 하기 위해 react-helmet-async를 쓴다고 한다.

  • 서버에서 비동기 작업을 수행할 경우 데이터 요청 별로 Helmet을 캡슐화 하기위해 사용하는 패키지라고 나와있다. 그냥 Helmet을 캡슐화 해준다고 생각하면 편하다. ⏤ reference | zero_woo의 개발블로그

📒 아쉬운 점

기획

기획 부분에 많은 시간을 쏟지 못하고 바로 개발에 들어갔던 것이 조금 화근으로 이어진 것 같습니다. 개발할 때 마다 필요한 디자인이 생겨서 디자이너분을 귀찮게 만드는 일들이 생겼고, API 를 수정해야하는 일들도 일어났습니다. 그리고 제일 중요한 컴포넌트 A를 B에도 적용시킬 수 있을 것이라고 생각했는데 로직이 달라서 그렇지 못한 부분 또한 있었습니다.. 🥲 기획을 조금 더 세심하고 면밀히 할 시간을 가지고 로직을 팀원들과 공유했다면 어땠을까라는 생각을 했습니다.

소통

팀 프로젝트에서 소통을 아쉬운 점으로 꼽는 것은 굉장히 크리티컬한 일임을 인지한다. 만약, 타이틀만 본다면 역량이 부족하다고 생각하게 될 것 이라는 것도 인지하고 작성하는 바다. 그럼에도, 다른 프로젝트에 참여하게 된다면 이러지 않기 위해 작성하는 것이 이유다.

초기 기술스택을 선정할 당시, 프론트엔드 팀원들 모두 Javascript를 인지하고, HTML 마크업에 대해 능숙하다는 섣부른 판단을 했다. 그리고 러닝 커브에 대해 신경쓰지 못하고 기술스택을 선정했던 것 같다. 결과적으로 지언님의 역량을 100% 발휘시키지 못했다. 러닝 커브가 빠른 Vue를 선택하면 어땠을까라는 생각을 한다. 결과적으로 짜여진 일정이 있었기 때문에 많은 파트를 분배해드리지 못하고 자바스크립트를 스터디를 하는 방법을 선택했다.

마크업

html 시간에 협업에서 분명히 시맨틱한 마크업에 대해 배웠고, 지금까지 프로젝트에서 잘 적용시키고 있었다고 생각한다.

같은 컴포넌트라고 판단하고 구현을 했는데 오른쪽(회원가입)의 컴포넌트를 감싸고 있는 태그가 form이었고, 왼쪽(프로필편집)의 관심사에서부터 한줄 소개를 감싸는 태그도 form이었다. form 내부에 똑같은 태그가 올 수 없기 때문에 div로 감싸주어야 하는 상황이 발생했다... ⏤ 결론적으로 컴포넌트의 실행 로직이 달라서 해당 컴포넌트를 사용하지 못했고 시간 내에 프로필 편집 기능을 구현하지 못했다. ⏤ 이러한 문제를 근본적으로 해결하기 위해 자꾸만 div 태그를 쓰게 되었던 것 같다.

그래도 나름의 최소한의 마크업은 맞추고자 했다.

  • 페이지 이동은 a , 그 외 버튼은 button

    업로드 버튼가 바로 업로드 페이지로 이동하는 것이였다면 a를 사용했을 텐데 모달을 띄워주는 역할을 해야하기 때문에 button 으로 했다.

  • 의미가 있게 사용되는 이미지라면 img , 아니라면 div 태그의 'background-image'를 사용했다.

조금 더 다양한 html 태그를 익혀서 접근성에 진심인 사람이 되고 싶고 더 나은 세상에 기여할 수 있는 개발자가 되고 싶다.. !!

많았던 모달의 처리 방법

페이지마다 모달이 적으면 하나 많으면 4개도 있었다.
새로고침할 때 다시 모달이 있었으면 하는 마음에 redux로 우선적으로 했는데 지금 생각해보면 굳이..? 라는 생각이 든다. redux로 관리하다보니 모달 state를 false를 해주지 않는다면 새로고침해서 엉뚱한 모달이 뜨는 현상이 발생했다. 그리고 버튼 한 개 클릭해도 두 개의 모달이 뜨는 처참한.. 상태까지 발생했다. 이걸 고치는데 시간이 더 오래 걸렸다. 모달 상태 관리가 조금 아쉽습니다.. !

📄 파일구조

생각보다 많은 페이지(?) 와 컴포넌트로 이미지 파일을 빼도 좀 많은 것 같다...

├── README.md
├── craco.config.js
├── jsconfig.json
├── package-lock.json
├── package.json
├── public
│   ├── _redirects
│   ├── assets
│   └── index.html
├── src
│   ├── App.jsx
│   ├── ReactHelmet.jsx
│   ├── api
│   │   └── request.js
│   ├── components
│   │   ├── carousel
│   │   │   └── carousel.js
│   │   ├── chatting
│   │   │   └── ExitButton.jsx
│   │   ├── common
│   │   │   ├── Spinner.js
│   │   │   ├── Template.js
│   │   │   ├── button
│   │   │   │   ├── assets
│   │   │   │   └── index.jsx
│   │   │   ├── categories
│   │   │   │   └── index.jsx
│   │   │   ├── modal
│   │   │   │   ├── assets
│   │   │   │   └── index.jsx
│   │   │   ├── titlePrevHeader
│   │   │   │   └── index.jsx
│   │   │   └── webViewError
│   │   │       └── index.jsx
│   │   ├── header
│   │   │   ├── assets
│   │   │   ├── index.jsx
│   │   │   ├── mainHeader.jsx
│   │   │   └── profileHeader.jsx
│   │   ├── interests
│   │   │   └── index.jsx
│   │   ├── modalBackground
│   │   │   └── index.jsx
│   │   ├── myPageVideoList
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── myVideoEdit
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── navigator
│   │   │   ├── assets
│   │   │   ├── data
│   │   │   │   └── menu.js
│   │   │   └── index.jsx
│   │   ├── step1
│   │   │   ├── ProfileForm.jsx
│   │   │   └── modal.jsx
│   │   ├── step2
│   │   │   └── SelectInterest.jsx
│   │   ├── video
│   │   │   └── index.jsx
│   │   ├── videoItem
│   │   │   └── index.jsx
│   │   └── videoList
│   │       └── index.jsx
│   ├── data
│   │   └── kakao.js
│   ├── hooks
│   │   └── infiniteScroll.js
│   ├── index.js
│   ├── pages
│   │   ├── 404Error
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── Interests
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── accountManage
│   │   │   └── index.jsx
│   │   ├── alarm
│   │   │   └── index.jsx
│   │   ├── chatScreen
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── chatting
│   │   │   └── index.jsx
│   │   ├── detail
│   │   │   └── index.jsx
│   │   ├── file-upload
│   │   │   ├── FileUploadPage.jsx
│   │   ├── intro
│   │   │   └── index.jsx
│   │   ├── login
│   │   │   ├── KakaoRedirectPage.jsx
│   │   │   ├── LoginCallbackPage.jsx
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── logout
│   │   │   └── index.jsx
│   │   ├── main
│   │   │   ├── assets
│   │   │   │   └── img.svg
│   │   │   └── index.jsx
│   │   ├── mypage
│   │   │   ├── assets
│   │   │   │   └── upload.svg
│   │   │   └── index.jsx
│   │   ├── notYetPage
│   │   │   └── index.jsx
│   │   ├── profile
│   │   │   ├── assets
│   │   │   └── index.jsx
│   │   ├── profileEdit
│   │   │   ├── assets
│   │   │   ├── index.jsx
│   │   │   └── vaildation.js
│   │   └── step
│   │       ├── RegisterSuccessPage.js
│   │       ├── Step1Page.js
│   │       ├── Step2Page.js
│   │       ├── assets
│   │       ├── assets-register-success
│   │       ├── assets-step1
│   │       └── assets-step2
│   ├── redux
│   │   ├── reducers
│   │   │   ├── modal.js
│   │   │   ├── navigator.js
│   │   │   ├── user.js
│   │   │   ├── userAccessCount.js
│   │   │   └── video.js
│   │   └── store
│   │       └── store.js
│   ├── routes
│   │   └── index.jsx
│   ├── style
│   │   ├── globalStyle.js
│   │   ├── index.js
│   │   └── theme.js
│   └── utils
│       ├── calAge.js
│       ├── calDate.js
│       ├── interestColor.js
│       ├── selectOptions.js
│       └── toggleHeartState.js
└── yarn.lock

👋 마무리하며

이리 저리 4주간 고생했지만 부족한 것 같은 프론트엔드의 코드는
luvShort_frontend git repository
에 알차게 담아놨습니다 ✨

프로젝트 마지막주에 얼마나 .. 치열했는지 Git 잔디 밭이 설명해주는 것 같다. 주말엔 코테랑 과제 전형 병행하면서 잠을 자는둥 마는둥 했는데 몇일은 편하게 발 뻗고 잘 수 있을 것 같다. 🙏

profile
이전해요 ☘️ https://mei-zy.tistory.com

0개의 댓글