IT일상

우아한테크코스 팀 프로젝트 2차 스프린트 회고 본문

우아한테크코스

우아한테크코스 팀 프로젝트 2차 스프린트 회고

solo5star 2023. 8. 8. 22:30
리뉴얼 된 블로그로 보기: https://solo5star.dev/posts/49/

 

우아한테크코스의 팀 프로젝트는 총 5개의 스프린트로 이루어져 있습니다.
각 차수마다 2주간의 스프린트 기간을 가지고, 구현한 내용을 데모데이 때 발표합니다.

 

 

MVP를 만들기 위한 여정

MVP란, 최소 기능 제품(Minimum Viable Product)을 말하는 것이며, 최소한의 기능을 구현한 제품이다. 최소한의 기능이라는 것이 중요한 포인트인데, 완성도가 낮더라도 프로젝트의 핵심 컨셉을 빠르게 구현하고 고객으로부터 검증을 받아야 하기 때문이다. 숏폼 형식으로 앱을 완성하였는데 막상 시장의 반응이 좋지 않다면? 이미 많은 노력을 쏟은 시점에 방향을 바꾸는 것은 많은 것들을 물거품으로 바꿀 것이다.

 

요즘카페의 핵심 컨셉이란 무엇인가? 성수동의 시각적으로 매력적인 카페를 숏폼 형식으로 탐색할 수 있게 해주는 것이다. 우리는 이 아이디어를 검증하기 위해 빠르게 MVP를 만들고 고객의 반응을 볼 것이다.

 

그렇지만! 본격적인 프로덕트를 만드는 것은 처음이기 때문에... 생각한대로 되지는 않았던 것 같다. 많이 삐걱거렸다. 그렇지만 실수하면서 배운 것도 많았다고 생각한다.

 

 

프로젝트 준비하기

1차 스프린트동안 기획이 어느정도 가닥이 잡히고 팀원간의 싱크가 이루어졌다. 이를 바탕으로 프로젝트를 시작하게 된다.

 

본격적으로 코딩을 하기 전에 준비해야 할 것들이 많다. 어떤 기술 스택을 사용할건지? 코딩 컨벤션은 어떻게 할 것인지? 번들러 셋업은 어떻게 할 것인지?

 

번들러는 webpack을 사용한다. 우아한테크코스에서의 요구사항이기 때문이다. webpack 외에 다른 번들러를 사용할 수 있었더라면, vite를 사용하였을 것이다. 개인적으로 vite를 사용했을 때, 세팅을 만져줄 것이 거의 없었던 데다가 esbuild를 기반으로 하여 정말 빠르게 실행되었기 때문이다. 맥북 유저는 큰 차이를 잘 못느끼는 것 같지만 성능이 좋지 못한 윈도우 노트북을 사용하는 입장에선 차이가 크게 느껴졌다...

 

 

webpack 셋업

const API_URL = process.env.API_URL ?? 'http://localhost:8080';

/** @type {import('webpack').Configuration} */
export default {
  mode: 'development',
  // https://github.com/TypeStrong/fork-ts-checker-webpack-plugin#installation
  context: dirname(fileURLToPath(import.meta.url)),
  entry: './src/index',
  output: {
    filename: 'bundle.js',
    publicPath: '/',
  },
  resolve: {
    extensions: ['.js', '.jsx', '.ts', '.tsx'],
  },
  module: {
    rules: [
      {
        test: /\.[tj]sx?$/i,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
    ],
  },
  plugins: [
    new DotenvWebpackPlugin({
      systemvars: true,
    }),
    new HtmlWebpackPlugin({
      template: 'index.html',
      filename: 'index.html',
    }),
    new CopyPlugin({
      patterns: [{ from: 'public', to: '' }],
    }),
    new ForkTsCheckerWebpackPlugin(),
  ],
};

트랜스파일링으로는 babel을 사용하기 때문에, babel-loader를 사용한다. 물론, 프로젝트에서 TypeScript를 사용하기 때문에 ts-loader라는 옵션도 있었지만 타입 체킹과 트랜스파일링 과정 자체를 독립적으로 수행하게 하고 싶었기 때문에 구태여 babel을 설치하고 babel-loader를 사용하였다.

 

트랜스파일링을 물론 babel을 향후 esbuild로 교체해보고 그 차이를 실감해보자는 큰 그림을 그리고 있었던 것도 있었다.

 

플러그인들은 개발을 진행하며 하나씩 추가하게 되었는데 각각의 목적은 다음과 같다.

* dotenv-webpack: .env 파일을 통해 환경변수를 설정하기 위해

* html-webpack-plugin: .html 파일에 번들된 자바스크립트 파일을 script 태그로 주입하기 위해

* copy-webpack-plugin: 정적 리소스(static resource)들을 사용하기 위해

* fork-ts-checker-webpack-plugin: 타입 체킹을 위해

 

babel-loader에서는 타입 체킹을 수행하지 않는다. 대신, fork-ts-checker-webpack-plugin를 통해 타입 체킹을 수행하게 된다. 사실 처음엔 이 플러그인 없이 개발을 진행하였는데 webpack 번들링 과정에서 타입 오류를 띄워주지 않았다. 이 플러그인을 사용하면 babel 트랜스파일링과는 별개의 프로세스에서 타입 체킹을 수행해주기 때문에 빌드 시간을 절약할 수 있다.

 

 

패키지 매니저 선정

npm을 쓸지, yarn을 쓸지 고민했다. 예전에는 yarn이 npm 대비 좋은 성능으로 많이 사용했었는데 최근에는 큰 차이가 없다고 한다. 더군다나 작은 규모의 프로젝트이기 때문에 차이를 느끼기는 것은 더더욱 어려울 것이다.

 

출처: https://www.pixelmatters.com/blog/yarn-npm-or-pnpm

팀원이 yarn을 사용해 본 경험이 없다고 하여 yarn을 써보는 것도 나쁘지 않다고 생각했다.

 

https://github.com/yarnpkg/yarn

나중에 알게된 사실이지만 yarn에는 1,2,3 버전이 있고, yarn 1이 yarn classic, yarn 2, 3이 yarn berry라고 한다. yarn 레포지토리에서 yarn 1은 버그 픽스 정도만 할 예정이며 최신 버전의 yarn 사용을 권장하고 있다. 향후 npm, yarn berry, pnpm의 대안을 생각해보아야 겠다.

 

 

테스트 도구 선정

테스트 도구를 선정하기 전, 테스트 도구가 왜 필요한지부터 생각해보았다. 프로젝트에서 기능을 지속적으로 추가하면서 우리의 핵심 가치가 여전히 동작하는지를 확인할 때 유용할 거라 생각했다. 그렇지만 프로젝트 초기 단계에서 피드백을 받으며 변경이 자주 일어날 수 있다. 어쩌면 우리의 핵심 가치가 변경될 수도 있다.

 

테스트를 작성하는 것도 시간이 든다. 이러한 단점들로 프로젝트 초기에는 테스트를 작성하는 것이 적합하지 않다고 생각되어, 후순위로 미루게 되었다. 그렇지만 프로덕트가 어느정도 안정되면 cypress를 사용하여 E2E 테스트를 작성해 볼 예정이다.

 

Storybook은 컴포넌트 단위로 피드백을 받기에 아주 좋은 도구이다. 실제로 우테코 미션을 진행하며 많은 유용함을 느꼈다. 그렇지만 프로젝트 초기에는 컴포넌트 단위로 개발하는 것 보단 전체 그림을 보며 만드는 탑-다운 방식으로 작업할 일이 많다 보니 초반에는 잘 사용하지 않았다. 향후, 동작을 테스트하기 위해 로그인이 필요하거나 특정 조건이 일치하여야 하는 식으로, 뎁스가 깊어진다면 유용할 것으로 보인다.

 

MSW는 백엔드 개발과 프론트엔드 개발이 병렬적으로 이루어지기 위해 필수라고 할 수 있다. API 응답을 모킹하여 백엔드가 완성되지 않은 상태에서도 전반적인 동작을 테스트할 수 있다. 물론 백엔드가 완성이 된 상태라 하더라도 프론트엔드만 띄워두고 독립적으로 테스트하는 데에도 유용하다.

 

 

스타일 라이브러리 선정

CSS-in-JS 방식을 사용하는 styled-components를 채택하였다. 처음 고민을 할 때 emotion과 비교하였었는데 사실상 큰 차이는 없다고 판단되어 팀원들의 친숙도가 높은 styled-components를 사용하였다.

 

const CafeSummary = ({ title, address, onClick }: CafeSummaryProps) => {
  return (
    <Container onClick={onClick} role="button" tabIndex={0}>
      <Summary>
        <Title>{title}</Title>
        <Address>
          <LocationPinIcon />
          {address}
        </Address>
      </Summary>
      <ButtonList>
        <Button>
          <SolidInfoCircleIcon />
          <ButtonText>더보기</ButtonText>
        </Button>
      </ButtonList>
    </Container>
  );
};

styled-components에서는 HTML Element + CSS의 조합으로 컴포넌트를 만든다. 위의 코드 스니펫에서 Container, Summary, Title 같은 것들이 해당된다.

 

태그만 보았을 때 어떤 의미로 사용되었는지 쉽게 파악할 수 있는 점이 styled-components의 장점이라 생각한다. div, div, span 과 같은 식으로 작성하였다면 구조를 이해하는데 많은 시간이 들겠지만, Summary, Title, Address 등 태그 자체에 의미를 표현함으로서 전체 구조를 쉽게 파악할 수 있게 해준다.

 

CSS-in-JS는 CSS와 자바스크립트의 결합이 매우 편하여 생산성이 높지만 CSS 코드가 JavaScript에 그대로 들어가기 때문에 번들링 사이즈가 커지고 런타임에 영향을 준다는 점이 단점으로 꼽힌다. 그럼에도 생산성이라는 이점이란 것이 굉장히 크기 때문에 단점을 감수할만 하다.

 

최근에는 CSS-in-JS의 이러한 단점을 극복하기 위해 Zero-runtime이란 개념도 등장했다. CSS를 컴파일 타임에 추출하여 런타임에 포함되지 않도록 해준다고 한다. 대표적으로 Vanilla Extract, Linaria가 있는데 아직 생소하여 미처 도입하진 못했다.

 

 

상태 관리 라이브러리 선정

최신 유행이라고 하는 react-query와, 우테코에서 미션을 할 때 사용해본 recoil과 비교해보았다. 사실 둘은 컨셉부터가 많이 다르다.

 

recoil은 전역 상태 관리 라이브러리이다. React Context API를 쓰는 것처럼 recoil로 전역 상태를 사용할 수 있는데 상태 단위가 atom이다. userAtom, articleAtom 처럼 상태 하나에 대한 atom을 만들 수 있다.

개인적으로 recoil은 전역 상태를 관리하는 관점에서는 좋았지만, 서버에서 받아오는 데이터를 처리하기엔 썩 좋지 못헀다. recoil에서도 Loadable, selector 과 같이 비동기 상태를 관리할 수 있는 방법을 제공하지만 완벽하진 않았다.

 

1. 아직 0.x.x 버전이며, UNSTABLE API가 많다.

2. 데이터를 새로 불러오기 위해 refresh를 하면 잠깐 로딩 스피너가 표시되는 시간이 생긴다.

3. 기본적으로 멱등한 응답이라 간주하고 캐싱한다. POST, DELETE 등 effect를 발생시킬 땐 별도로 구현해야 한다.

 

특히나 거의 모든 데이터를 서버에 저장하는 프로젝트 특성 상 recoil의 사용은 고통스러울 수 밖에 없다. 반면 react-query는 기본적으로 비동기 상태를 관리해주는 라이브러리이기 때문에, recoil을 사용함으로서 고통받는 부분을 해소할 수 있다.

 

react-query의 장점을 짧게 소개하자면,

1. 비동기 상태를 관리해준다. (loading, error, success, idle)

2. stale-while-revalidate를 채택하여 두 번째 fetch부터는 로딩 화면이 보이지 않는다.

3. suspense와의 연계가 쉽다.

4. useInfiniteQuery, refetchOnWindowFocus 등 유틸리티성 기능들을 많이 제공해준다.

 

recoil을 사용할 때 워낙 고통스러웠던 것도 있었고, react-query의 서버 상태 관리라는 관점이 프로젝트에 알맞다고 생각하여 react-query를 채택하였다.

 

이 라이브러리를 사용해보았을 때 개인적으로 굉장히 좋은 개발자 경험(DX)을 제공해준다고 느꼈다. 그동안 전역 상태라는 관점으로 프로젝트를 했었는데 react-query를 사용해보며 서버 상태 관리라는 관점으로 바꾸어 생각해보니 많은 것이 편해진 것 같다.

 

 

Git 브랜치 전략

GitHub Flow를 채택하였다. 프로젝트 규모가 크지 않기 때문에 main 브랜치 하나만 두고 feature, bugfix 브랜치를 만들어 작업한 뒤 main에 머지하는 방식이다.

 

main에 머지할 때는 반드시 GitHub PR을 통해 머지하도록 한다.

 

PR을 머지하기 전에 리뷰도 꼼꼼히 하도록 한다. 이 과정을 거침으로서 팀원들이 좀 더 일관성 있게 코드 퀄리티를 유지(싱크)할 수 있을 것이다.

 

플로우를 정리하자면,

1. 진행해야 할 작업에 대해 GitHub에서 이슈를 생성한다.

2. 브랜치를 생성하고 커밋을 한다.

3. 브랜치를 GitHub에 push하고 PR을 생성한다.

4. 팀원의 코드 리뷰를 받고 approve가 된 후 merge한다.

 

 

OAuth 로그인 구현

로그인을 먼저 구현한 것은 실수 중 하나이다(...) 애자일에서는 고객에게 빠르게 가치를 전달하기 위해 핵심 기능을 먼저 구현해야 한다. 그렇지만 이렇게 한번 실수해봄으로서 얻는 것도 있었다고 생각한다.

 

아무튼, 카카오 및 구글 OAuth 로그인을 지원하기 위해 OAuth 설정 및 OAuth 플로우를 함께 논의하고 각자 파트(FE, BE)에서 구현을 진행하였다. 사용자 입장에서는 로그인 버튼만 누르고 카카오(또는 구글) 로그인만 해주면 되기 때문에 간단하게 느껴질 수 있지만, 뒤에서는 많은 일이 일어나고 있다.

 

Authorization Code 플로우로 구현하였는데, Client에 Authorization Code를 주면 그걸 다시 백엔드에게 주고 우리 서비스에서 사용 가능한 Access Token을 요청하는 식이다.

 

Authorization Code는 비교적 유효 기간이 짧으며 (1~10분) 카카오(또는 구글)의 Access Token이 Client에 직접적으로 노출되지 않기 때문에 보안 측면에서 좋다. (Client에 전달되는 Access Token은 Server에서 발급한 것이다)

 

Client에서 OAuth 로그인 페이지로 이동하기 위해서 OAuth 로그인 URL이 필요한데, 이는 Server에 요청하면 응답해주는 방식으로 논의하였다. Client 측에서 OAuth의 Client ID와 같은 정보를 관리하는 것이 번거롭다고 생각했기 때문이다. 플로우에서 요청이 하나 더 늘었지만 관리할 포인트를 줄이는 것과 충분히 트레이드-오프 할 수 있다 생각한다.

 

 

CI/CD

로그인에 이어 두 번째 실수... CI/CD보다 핵심 가치를 구현하는게 우선이었다.

대상이 사용자가 되었든 개발자가 되었든 시간을 단축시켜주고 편리하게 하는 기술은 언제나 가슴이 두근거리는 법. CI/CD는 개발자를 편하게 해주기 때문에 내가 좋아하는 것 중 하나이다.

 

슬래시(/)로 나눠둔 것처럼, CI와 CD 두 개를 나누어 볼 수 있다. CI는 지속적 통합(Continuous Integration), CD는 지속적 배포(Continuous Deployment)이다.

 

테스트->빌드 과정까지가 CI라 할 수 있고,

빌드 결과물을 실제 서버에 전송하고 반영하는 것이 CD라 할 수 있다.

 

GitHub Actions와 TeamCity 두 개를 사용하였는데, GitHub Actions는 GitHub와의 통합이 쉬워서 PR이나 커밋이 올라왔을 때 빌드해보고 잘 되는지 테스트해보는 용도로 사용한다. TeamCity는 다소 생소할 수 있는데, intellij에서 만든 CI/CD 도구이다.

 

TeamCity는 비슷한 것으로 Jenkins와 비교할 수 있는데, 둘 다 self-hosted이기 때문이다.

 

개인적으로 소규모 프로젝트의 CI/CD는 쉽고 빠르고 간단한 것이 제일이라 생각한다. 빌드하고 배포하는 데에 섬세한 설정을 해줄 필요는 없다고 생각한다. 이런 관점에서 Jenkins는 체크박스가 너무 많았고 설정하려면 이를 다 이해하고 넘어가야 했지만, TeamCity는 입력해줄 것이 거의 없어 설정을 빠르게 추가할 수 있었다.

 

UI가 굉장히 깔끔하다는 것도 장점. UI가 예뻐야 자주 들어가게 되고 관심도 가지게 된다. 안 중요할 수 있지만 중요하다 생각하는 부분.

 

파이프라인은 위와 같이 동작한다. TeamCity는 Server와 Agent로 구성되어 있는데, Agent에서 docker container를 생성하고 빌드를 한다. 빌드된 결과물은 SCP 명령으로 대상 서버에 전송된다.

 

백엔드(Spring)의 경우 배포를 할 때 기존 실행중이던 프로세스가 8080포트를 점유하고 있기 때문에 종료해야 한다. 종료 후 새로 빌드한 따끈따끈한 Spring 서버를 nohup 명령으로 실행한다. (Spring이 dockerize가 안되어있는데 빨리 dockerize되면 좋겠다..)

 

프론트엔드는 빌드 수행 시 출력이 정적 파일(.html, .js, .css)들이기 때문에 배포가 간단하다. Nginx를 docker로 띄워두고 서빙되는 디렉토리에 파일만 새 걸로 교체해주면 된다.

 

 

프론트엔드 기능 구현

핵심 가치에 가까운 기능들을 구현했다. 카페 이미지를 한 화면에 꽉 채워서 보여주고 이를 스크롤하며 탐색하게 해주는 기능인데, CSS의 scroll-snap을 사용하여 쉽게 구현할 수 있었다.

또, react-query의 useInfinityQuery와 직접 구현한 useIntersection 훅을 사용하여 무한 스크롤을 구현하였다.

 

백엔드의 구현이 미완성이기 때문에 MSW로 API를 모킹하고 테스트하며 만들었다. 아쉽게도 이번 스프린트에서는 백엔드와의 연동을 끝내 이루지 못했다.

 

 

2차 스프린트에서 얻은 것

핵심 기능도 완성되지 못한 상태에서 로그인, CI/CD를 먼저 한 것은 과연 옳았는지 다시 한 번 생각해보는 시간을 가졌다. 해야 할 일을 이슈로 만들고 각자 하고 싶은 일을 맡아서 진행하였는데, 사람들이 로그인이나 CI/CD를 해보고 싶었던 건지 핵심 기능보다 로그인, CI/CD가 먼저 완성이 되었다.

 

우리의 핵심 가치가 무엇인지 생각해보고 우선순위를 정하는 것이 중요하다는 것을 깨달았다. 학교에서도 애자일에 대해 이론으로는 배웠었지만 이렇게 피부로 직접 경험해보니 애자일에서 중요한 게 과연 무엇인지 알게 된 것 같다.

 

학교에서 애자일이랍시고 했던 것. 알고보니 그냥 워터폴입니다

Comments