내 작고 소중한 첫 팀프로젝트...*(feat. 새벽 세시)

Gorae·2022년 3월 17일
1

스몰 프로젝트

목록 보기
9/9
post-thumbnail

대학에서 그렇게 많은 팀 과제를 했으면서, 개발 팀 프로젝트는 쉽게 용기가 나질 않았다. 객관적으로 내 실력을 평가하기에 앞서 늘 부족함이 먼저 보였기 때문이었는데, '1차 마무리' 정도 끝낸 시점에서 그때를 되돌아보면 '좀 더 빨리 용기 낼걸'하는 생각이 든다. 팀원들도 나도 전부 협업은 처음이라, '척척'보다는 '우당탕탕'이 훨씬 어울리는 시간이었다. 그래도 얻은 게 많았기에, 더 늦기 전에 정리해 보려 한다.

어떻게 시작하게 되었는가?

JAVA를 공부 중인 지인들과 마음이 동했다. 연초에 팀 프로젝트 하나를 하면 좋겠다는 생각이었고, 이야기를 꺼내자마자 시작 날짜를 잡았다. 그렇게 백엔드 2명, 프론트 1명(본인)으로 팀이 꾸려졌다.

그래서 만들기로 한 것

백과 프론트가 함께 유의미한 작업을 할 수 있는 사이트를 클론 하기로 했다. 맥딜리버리, 텀블벅 등을 생각했는데, 다음과 같은 기준으로 사이트를 정했다.

  1. 4-5주 이내에 작업 가능한가
  2. 백과 프론트가 적절히 통신하는가

언어는 JAVA, React 로 처음부터 정해놓고 시작을 했고, 만장일치로 회원가입/로그인, 주문-결제-주문조회 로직 구현이 가능하고 UI 가 비교적 단순한 새벽다섯시 사이트를 클론 하기로 했다.
팀명은 우리 3명 + 새벽 3시까지 공부하자는 패기로 '새벽세시'로 지었다 🔥

어떻게 진행했는가

1. 소통 도구 : 슬랙(Slack), 게더타운(Gather.town)

처음엔 구글밋으로 작업했고, 게더를 알고 난 후부터는 쭉 게더에서 작업했다. 10시~16시까지 코어 시간을 정한 뒤, 따로 작업을 하더라도 접속해있었다.(장점: 귀여움)

프로젝트 계획은 핵심 기능, 부가 기능, 추가 기능으로 구분하여 각자 생각해 본 후에 슬랙에서 결과를 추려보는 방식으로 진행했다. 이후 공통적으로 학습해야 하는 것도 같은 방식을 이용했는데, 괜찮은 방법이었던 것 같다.

2. 문서화 도구 : 노션(Notion)

그리고 노션에 매주 일정을 예상 소요 시간, 실제 소요 시간을 기록하며 계획했다. 소요 시간을 가늠하기가 쉬운 일이 아니었지만, 현업에서 꼭 필요한 능력이란 생각은 들었다.

프로젝트 계획부터 DB, API 정의까지 노션에 정리하며 진행했는데, API 정의는 이후 Postman 을 활용하는 방안으로 수정했다. 테스트할 수 있는 기능, 가독성 등에서 Postman 이 훨씬 좋았기 때문이다.

어려웠던 부분

사실 이걸 적기 위해 이 글을 쓰는 거다. 첫 협업이었고, 정처기 자격증 공부할 때 이후로 JAVA 코드를 본 적도 없었기에, 정말 자신감만 가득했다. 지금에서야 '이런 게 어려웠다니!' 할 수 있는데, 다행히 그때보다 나아졌음을 온몸으로 느끼기에, 어설픈 고민들도 기록해두려 한다.

❓ 리액트랑 자바랑 어떻게 한 프로젝트에서 협업해?

두 언어의 연결을 고민할 문제가 아니라, 서로 다른 port 를 사용할 때 이를 연결하는 방법을 고민하면 되는 것이었다. 결론은 CORS 문제만 해결해 주면 되는 것.

❗️CORS란 ?
교차 출처 리소스 공유(Cross-Origin Resource Sharing). 브라우저에서는 보안상의 이유로 cross-origin HTTP 요청을 제한한다. 서버에서 이 요청을 허락한다면 브라우저에서도 허락할 수 있도록 하는 것이 CORS이다.
cross-origin 이란 다음 세 가지 경우를 말한다

  • 프로토콜이 다른 경우 (ex. http 와 https 는 서로 다른 프로토콜)
  • 도메인이 다른 경우
  • 포트 번호가 다른 경우

CORS 문제는 요청 헤더를 수정하거나, proxy 서버를 두는 등의 방법으로 해결할 수 있는데, 리액트에서는 package.json 에 proxy 를 추가해주기만 하면 됐다. 이렇게 하면 webpack 에서 알아서 proxy 기능을 수행해준다.

참고 - CORS mdn / CORS란 무엇인가? / 리액트 CORS 처리

❓ JWT 토큰이 뭐지?

비회원 로직에 앞서 회원 로직을 구현하기로 했기에 그 시작이 로그인이었는데, JWT 토큰이 뭔지도 몰랐던 나로서는 이해부터 해야 했다. 각자 이해하고 있는 내용이 다르지 않도록 시간을 정해서 공부한 뒤에, 서로 설명해 주는 방식으로 함께 이해했다.

그리고 스몰 로그인 프로젝트를 만들어 토큰을 주고받는 로직만 먼저 만들어봤다. 이땐 팀프로젝트라기 보단 스터디에 가까웠다. 그래도 스스로 코드를 작성하고 이해할 수 있게 돼서 행복했다.

공부한 내용은 여기서 정리했다 👉🏻 JWT 토큰 인증에 대하여

❓ 토큰 로직은 이해됐는데, 프론트에서 처리하는 법을 모름

accessToken, refreshToken 다 이해했는데, 토큰 만료 시간을 어떻게 알아채고 재발급 요청을 보내야 하는가에 대해 고민이 생겼다. 이는 axios interceptors를 알게 됨으로써 해결할 수 있었다.

axios는 요청을 가로챌 뿐만 아니라 응답도 가로챌 수 있어서, 모든 요청에서 토큰의 만료를 알아채고 재발급 받을 수 있다.

문제는 요청하지 않았을 때 만료된 토큰 때문에 에러가 생길 수 있는데, 이 부분은 재로그인을 요구하는 모달창을 띄우거나, refreshToken 만료 시간을 아주 길게 잡거나 하는 등으로 해결할 수 있었다. 여기서는 후자를 썼는데, 보안 측면에서는 전자가 낫고, 유저 측면에서는 후자가 낫다고 생각됐다.

// interceptors.js
import axios from 'axios';
const API_URL = 'http://localhost:8000';

const onRequest = (config) => {
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
};

const onRequestError = (error) => {
  return Promise.reject(error);
};

const onResponse = (response) => {
  return response;
};

const onResponseError = async (error) => {
  if (error) {
    if (error.response.status === 401) {
      const oldRefreshToken = localStorage.getItem('refreshToken');
      const userId = localStorage.getItem('userId');
      try {
        const res = await axios.post(`${API_URL}/auth/refreshToken`, {
          refreshToken: oldRefreshToken,
          userId,
        });
        const { accessToken, refreshToken } = res.data.data;
        localStorage.setItem('accessToken', accessToken);
        localStorage.setItem('refreshToken', refreshToken);
        return;
      } catch (_error) {
        console.log(Promise.reject(_error));
      }
    }
  }
};

const setUpInterceptorsTo = (axiosInstance) => {
  axiosInstance.interceptors.request.use(onRequest, onRequestError);
  axiosInstance.interceptors.response.use(onResponse, onResponseError);
  return axiosInstance;
};

export default setUpInterceptorsTo;

❓ React 에서 비동기 props 다루기

비동기로 받아지는 데이터를 props로 전달해야 할 때, data를 받아오지 못했다는 에러를 만났다. 이는 데이터가 다 받아지기 전까지 loading state 를 따로 관리하는 방식으로 해결했다.

// UserOrderPage.jsx
const UserOrderPage = ({ onClick, isOpen, showPrice, formatDate }) => {
  const [userOrderInfo, setUserOrderInfo] = useState();
  const [loading, setLoading] = useState(true);
  
// 중간 생략
  useEffect(() => {
    getOrderPage().then((result) => setUserOrderInfo(result));
    setTimeout(() => {
      setLoading(false);
    }, 500);
  }, []);

  if (loading) {
    return <Loading />;
  }

  return (
    <Section>
      <Header />
      <Main>
    // 중간 생략
      </Main>
      <Footer onClick={onClick} isOpen={isOpen} />
      <SideBar onClick={onClick} isOpen={isOpen} />
    </Section>
  );
};

suspense 를 쓰면 loading 상태를 따로 관리해야 하는 번거로움을 줄일 수 있다고 하는데, 아직 실험적 기능이니 차차 알아보고 적용하면 될 것 같다.
Suspense for Data Fetching

❓ 선택한 날짜를 조회하면 날짜가 1일 전으로 바뀌어있다?

자세히 보지 않았다면 잘 동작한다며 지나칠 수 있는 에러였다. 구현한 로직은 홈 화면에서 상품, 배송 날짜를 선택한 후 "장바구니에 담기" 버튼을 누르면, 선택 사항이 DB로 들어가게 되고, 장바구니 화면에서 DB에서 가져온 데이터를 보여주게 된다.

프론트에서 date 콘솔 창에 찍었을 때도, DB에서 확인했을 때도 날짜가 잘 들어가서 '도대체 어디서부터 잘못된 걸까' 생각했는데, DB에서 다시 데이터를 불러오는 과정에서 타임존(ex. UTC, KST)이 바뀌는 문제였다.

서버 시간, DB 시간, 프론트 시간 포맷이 다를 수 있다는 것을 처음 알았다. 날짜 포맷은 moment.js 를 사용하면 훨씬 간단하게 작업할 수 있었다.

import moment from 'moment';

const formatDate = (date) => {
  return moment(date)
    .add(9, 'hours')
    .format('YYYY' - 'MM' - 'DD')
    .slice(0, 10);
};

새로 알게 된 부분

💡 컴포넌트가 많아질 때 폴더를 분리하는 방법

리액트로 스몰 프로젝트를 해봤지만, 이렇게 컴포넌트가 많았던 적은 없었다.(처음엔 그럴 줄 몰랐다) 정해진 기준 같은 게 있는 건지, 훨씬 더 복잡한 프로젝트에서는 어떤식으로 하는 지 궁금했는데, 아래와 같은 답을 얻었다.

  • 2번 이상 쓰이는 컴포넌트라면 따로 생성
  • route 디렉토리 안에 컴포넌트 폴더를 만듦.

아래처럼 해당 라우트 관련 컴포넌트들을 모두 한 폴더에 넣었다.

💡 Auth Router 로 인증 라우트 구분하기

비회원/회원을 구분하여 라우팅을 처리해야 할 때, 각 컴포넌트에서 따로 로직을 작성하는 방법도 있겠지만, 라우트 자체를 따로 만드는 방법도 있음을 알게 됐다. 최상위 컴포넌트에서 인증 로직이 필요한 컴포넌트를 구분하기가 쉬우므로 가독성이 좋아졌다.
참고 소스 코드

// AuthRoute.jsx
import React from 'react';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';

const AuthRoute = ({ authUser, element }) => {
  return authUser ? element : <Navigate replace to="/login" />;
};

AuthRoute.propTypes = {
  authUser: PropTypes.bool.isRequired,
  element: PropTypes.node.isRequired,
};

export default AuthRoute;


// App.jsx
const App = () => {
  // 중간 생략
return (
  <>
		<Route
          path="/userOrder"
          element={
            <AuthRoute
              authUser={authUser}
              element={
                <UserOrderPage
                  onClick={toggleSideBar}
                  isOpen={isOpen}
                  showPrice={showPrice}
                  formatDate={formatDate}
                />
              }
            />
          }
        />
        <Route
          path="/notUserOrder"
          element={<NotUserOrderPage onClick={toggleSideBar} isOpen={isOpen} />}
        />
</>
);
};

💡 React에서 쿠키 사용하기

jwt 토큰을 어떻게 받아와서 어디에 저장할지 고민하던 단계에서, 쿠키로 토큰을 전달받는 방법에 대해 고민했다. 리액트에서는 관련 라이브러리를 설치하는 것으로 쿠키 값을 읽을 수 있었다.

💡 React Router v6 변경 사항

Switch 가 Routes 로 변경되는 등, 꽤 큰 변화가 있어서 적용시켰다.
React Router v5 -> v6

부족했던 부분

😱 DB, API 통신 설계를 구체적으로 하지 않은 채 와이어프레임을 작성했다

ERD와 API 정의, 로직 정의가 모두 이루어진 후에 프론트 와이어프레임을 작성했어야 했는데, 순서가 뒤죽박죽이었다. 프론트는 행위로직만으로 와이어프레임을 작성했고, DB 통신 없이 구현할 수 있는 것들을 먼저 만들기 시작했다. 백에서는 프론트 작업 우선순위에 따라 api 를 설계해서 공유했다.

😱 때문에 API, response 가 작업하면서 계속 수정됐다

알고리즘 문제 풀이를 할 때도 계획을 구체적으로 세운 뒤 코드를 작성하면 더 수월한 것처럼, 프로젝트도 구체적인 계획 하에 진행되어야 헤매지 않을 수 있음을 뼈저리게 느꼈다.

DB 테이블과 api response 가 작업 도중에 바뀌어버리거나, response 자체가 정의되지 않거나, 임시로 작성해 둔 response 값이 변경됐을 때 소통 오류로 에러를 뒤늦게 발견하거나, 순서가 잘못된 것만으로 수많은 어려움을 겪어야 했다.

😱 비회원 로직 구현 미완성

처음 계획에 핵심 기능으로 비회원 관련 로직이 모두 들어있었는데, 계속 꼬이는 로직으로 인해 마감 기한 내 작업이 불가하다고 판단, 회원 로직으로만 완성하게 됐다. 아래와 같은 고민들을 했는데, 기획 단계에서 이미 마쳤어야 할 고민이었다.

  • 가입 시 id 중복 체크는?
  • 비회원 주문번호 생성 방식은?
  • 비회원 로그인을 할 때 주문 비밀번호를 받을 것인가?
  • 비회원으로 장바구니 담기를 한 후에 로그인을 한다면?
  • 비회원 장바구니의 고유 식별값은?
  • 장바구니에 담긴 상품 유지 기간은?
  • 세일 등으로 인해 가격이 변경된다면?
  • 장바구니 상품 전체 주문 / 부분 주문
  • 날짜별로 장바구니 상품 담기
  • 회원 정보 조회 페이지에서 수정 없이 완료 버튼을 클릭한다면?

도전한 부분

✨ eslint, prettier 적용하기

VSC 에서 eslint, prettier 를 설치하는 것과 별도로 프로젝트 자체에서 eslint, prettier 설치를 해서 적용해야 한다는 것을 처음 알았다.(쓰기 부끄럽지만 그렇다...) 많이 쓰인다는 airbnb 규칙을 적용했는데, 수많은 에러에 당황했다.

하지만 그 과정에서 생각해 볼 부분이 많았다.

  • xxx is missing in props validation 에러
    prop-types 를 설치하여 적용하는 것으로 해결. Typescript 를 공부하고 있는 중이라, 타입 명시를 하는 연습이 더욱 유의미했다.
  • button type 요구 에러
    button 태그에 타입을 명시하지 않으면 default 값은 submit 으로 동작한다. submit 은 기본적으로 새로고침을 시키므로, type="button"을 씀으로써 단순 버튼 기능을 할 수 있게 된다. 참고 - 버튼에 타입을 쓰는 이유

✨ Styled-Components 쓰기

'Styled-Components' 써보라는 조언을 들었는데, 종종 사용 스택으로 언급되는 것을 봤었다. 찾아보니 CSS-in-JS 라이브러리 중 '가장 인기 있는'이란 말이 눈에 많이 들어왔고, 이 기회에 적용해 보는 것도 좋을 것 같았다.CSS가 애초에 막강하면 얼마나 좋아! CSS에 대해서 공부한 내용은 이 글에서 더 자세히 적었다( CSS-in-CSS / CSS-in-JS )

이전 프로젝트에서 CSS Modules로 작업했는데, 컴포넌트별로 CSS 파일이 존재하는 게 수정하기 번거롭다는 생각을 했다. Styled-Components 를 쓰면서는 그런 번거로움이 없어서 좋았고, js 코드처럼 사용할 수 있다는 점이 무엇보다 큰 장점 같았다.

요즘은 tailwindCSS 가 많이 눈에 들어오는데, 기회가 되면 써 볼 참이다.

✨ Docker 이미지 생성하기

Docker 를 처음 써봤다. DB, 백, 프론트(nginx)에서 각각 Dockerfile 을 생성하고, docker-compose 를 통해 한 번에 이미지를 생성할 수 있게 하는 과정까지 이해하게 됐다.

결과 화면 + github 링크

새벽 세시 프로젝트 github

마무리

프로젝트를 하면 할수록 공부할 것이 눈에 보였고, 매일 온몸으로 내 부족함을 깨닫게 됐다. 회고랍시고 썼던 글은 어디 내놓기도 민망한 일기 같은 느낌이었다.

결론적으로 계획했던 것을 모두 구현해 내진 못했지만, 혼자서는 절대 할 수 없었을 고민을 많이 하게 됐다. '이 정도로 안됐으면 포기하자'할 수 있을 일을, 프론트가 나 하나였기에, 같이 하는 프로젝트라는 책임감이 있었기에 끝까지 물고 늘어져봤다.

그리고 함께했던 팀원들이 좋아서, '협업이 이렇게 힘들구나'보다 '협업하니까 힘들어도 재밌다'라는 생각을 했다. 내 부족함에 예민해져서는 '좀 더 상냥하게 말할 수는 없었을까' 하는 후회도 했고, 좋은 사람을 만나길 바라기 전에 내가 좋은 사람이 되어야겠다는 다짐을 수차례 했다.

프로젝트 핵심 기능 외에 계획했던 부가 기능, 확장 기능 등 아직 남은 과제가 많은데, 이는 각자 상황이 된다면 조율해서 작업하면 좋을 것이다. 그땐 지금보다 더 성장한 모습일 수 있도록 노력해야지.

긴 글 읽어주셔서 감사합니다 🙏

profile
좋은 개발자, 좋은 사람

0개의 댓글