표준 개발 환경 개선 되돌아보기

Dec.28.2023 이기백

WEB Web Frontend

개발을 하면서 자신의 코드를 많이 보게 되지만 그보다 더 많이 보게 되는 것이 다른 사람 또는 팀이 작성한 코드입니다. 하지만 다른 사람의 코드를 보는 것이 항상 쉽진 않았을 것입니다. 각자 익숙한 방식으로 코딩을 하다 보니 다른 사람의 코드를 이해하기 어려운 경우가 있는데요. 팀 내에서도 비슷한 일을 겪고 있었습니다.

코어웹프론트개발팀은 올해 5월부터 새로운 팀 표준 개발 환경을 구축하기 시작해 4개월 간의 노력 끝에 완성하게 되었습니다.

이번 글에서는 왜 표준 개발 환경을 개선하게 되었는지, 어떤 과정을 거쳤는지, 좋았던 점과 아쉬웠던 점은 무엇인지 말씀드리려고 합니다.

같은 듯 다른 한 팀

처음 우아한형제들에 들어왔을 때는 팀 구성원이 5명 정도였고, 도메인도 푸드 주문에 한정돼 있었습니다. 기본적인 코드 스타일 가이드도 있었고 라이브러리도 React, styled-components와 같은 널리 사용하는 것이었습니다. 그래서 굳이 팀에서 사용할 표준을 정하고 관리할 필요를 느끼지 못했습니다.

하지만 시간이 흘러 결제 관련 도메인이 합쳐지고, 이어서 푸드의 모든 도메인을 맡게 되면서 팀은 어느새 16명이 되었고 팀 내 파트도 4개나 되었습니다.

각 파트에서 관리하는 프로덕트는 각자의 스타일에 맞게 작성되어 있었기 때문에 다른 파트 구성원이 도와주기 힘들었습니다. 또한, 팀원들로부터 가끔은 다른 도메인의 업무를 해보고 싶다는 의견이 많이 나왔습니다.

원래 하던 도메인과 다른 도메인의 업무를 하려면 비즈니스 이해도 물론 필요하지만, 기존에 작성된 코드를 다시 읽고 파악하는 시간이 필요합니다. 만약 코드 스타일, 사용하는 라이브러리, 디자인 패턴 등 개발 요소가 다르게 되어 있다면 어떨까요? 비즈니스 로직을 파악하기 전에 라이브러리 학습을 하는 데 시간을 소모하고, 익숙하지 않은 코드 스타일에 코드를 분석하는 시간이 몇 배는 더 들 것입니다.

이런 이유 때문에 표준 개발 환경을 만들어 신규 프로덕트부터 적용해 보면 어떻겠냐는 의견이 나왔고, 그렇게 표준 개발 환경 개선 TF가 생기게 되었습니다.

어떤 것들을 표준으로 정하면 좋을까?

코드 스타일

코드 스타일은 통일성과 가독성을 위해 꼭 지켜야 할 요소입니다. 코드 스타일이 통일되지 않으면 단 10줄의 코드라도 읽기 힘들어질 것입니다.

아래 코드는 동일한 기능을 하는 코드를 다른 두 가지 스타일로 작성한 예제입니다.

function div(a: number, b: number): number
{
    if (b===0) return;
    return a/b;
}

function div(a: number, b: number): number {
  if (b === 0) {
    return;
  }

  return a / b;
}

코드 스타일은 개인의 취향에 큰 영향을 받는 부분입니다. 어떤 사람은 스페이스를 이용한 들여쓰기를 좋아하지만, 다른 사람은 탭을 이용한 들여쓰기를 더 좋아합니다. 양쪽 모두 장단점이 있으며 어느 쪽이 특별히 낫다고 보기 어렵습니다. 이런 경우 표준을 정할 때 결정적인 근거가 없기 때문에 한 쪽을 정하기가 매우 어렵습니다.

전반적인 스타일은 코드 포맷터인 Prettier를 사용하여 맞추기로 했습니다. Prettier를 이용하면 코드의 AST1를 변경하지 않고, 즉 동작을 전혀 바꾸지 않고 코드 스타일만을 통일할 수 있습니다. 또한 코드 리뷰에서 코드 스타일을 맞춰달라는 번거롭고 불편한 리뷰를 달지 않아도 됩니다. 대부분 Prettier가 알아서 하지만, 몇 가지 취향적인 요소는 설정을 통해 바꿀 수 있도록 옵션을 제공하고 있어서 개인 또는 조직의 취향에 따라 정할 수 있습니다.

module.exports = {
  printWidth: 100,
  trailingComma: 'all',
  tabWidth: 2,
  semi: true,
  singleQuote: true,
  bracketSpacing: true,
  arrowParens: 'always',
  useTabs: false,
};

Prettier를 이렇게 설정하여 사용하고 있습니다

Prettier는 코드의 구조적인 스타일을 통일해 주지만, 변수명같이 자동으로 해줄 수 없는 부분도 있습니다. 이런 부분은 먼저 팀이 다 같이 모이는 회의 시간에 정하고 싶은 주제를 정리했습니다. 투표가 필요한 것들은 Slack의 팀 채널에 올리고, 조사가 필요한 것들은 담당자를 정해 리서치를 진행했습니다. 한 번에 정해진 것들도 많았지만, 열띤 논의가 펼쳐져 정하기 매우 어려운 것도 있었습니다. 논의에 너무 많은 시간을 쓸 수는 없었기 때문에, 각 의견이 평행선을 그리기 시작하면 투표를 통해 다수결로 정했습니다. 아래는 논의했던 주제 중 일부입니다.

  • 변수 네이밍 컨벤션: 상수는 항상 대문자 사용, 타입명에는 헝가리안 표기법을 사용하지 않음
  • Enum vs String Union: 런타임 코드에 변경이 있더라도 원래 의미에 맞는 Enum을 사용
  • React Component 파일 구성: 1 파일 1 컴포넌트 원칙 준수, 디렉토리는 도메인 별로 분리
  • void vs undefined: 직관적인 undefined 사용
  • 세미콜론 사용 여부: 항상 세미콜론 사용


Slack 채널에서 열띤 논의가 펼쳐졌습니다

정적 코드 분석

정적 코드 분석은 많은 버그를 미연에 방지할 수 있도록 도와줍니다. 또한 코드 스타일 자동화와 동일하게 코드 리뷰에서 낭비되는 시간을 줄일 수 있습니다. 특정 규칙을 추가하여 여러 오픈소스에서 실제 발생하는 버그를 고쳤다는 후기도 쉽게 확인할 수 있습니다.

정적 코드 분석 도구로는 이미 널리 사용되고 있는 ESLint를 사용하기로 했습니다. ESLint은 설정 파일을 통해 원하는 규칙을 설정할 수 있습니다. 최대한 많은 규칙을 지정하면 좋겠지만, 분석에 걸리는 시간이 길어질 수 있고 코딩의 자유도가 지나치게 떨어지는 등 개발 경험을 해칠 수 있습니다. 적당한 수준의 규칙을 정하기 위해, 분석에 오랜 시간이 소요되는 규칙을 미리 점검하였습니다. 또한, 규칙이 너무 느슨해지는 것을 방지하기 위하여 최초에는 최대한 엄격하게 규칙을 설정하고 이후 필요에 따라 합의를 통해 일부 규칙을 해제하는 것으로 결정했습니다.

ESLint에서는 수많은 규칙을 지원하고 있습니다. 그리고 필요에 따라 플러그인을 추가하면 규칙 수는 엄청나게 늘어나게 됩니다. 이런 것들을 하나하나 설정하면 너무 많은 시간을 써야 하기 때문에 비효율적입니다. ESLint를 설정해 본 개발자라면 누구나 이런 문제를 겪기 때문에 각자의 필요에 맞는 설정 프리셋을 작성하여 eslint-config-airbnb, eslint-config-standard와 같이 오픈소스로 공유합니다. 효율적으로 규칙을 설정하기 위해 설정 프리셋을 조사했고, Microsoft에서 진행하는 프로젝트인 Rush Stack이라는 곳에서 제공하는 설정 프리셋, @rushstack/eslint-config를 사용하기로 했습니다.

Rush는 Microsoft에서 개발한 모노레포 웹 프로젝트 관리 도구입니다. Rush와 함께 사용할 수 있는 다양한 구성요소를 Rush Stack이라는 이름으로 제공하고 있습니다. @rushstack/eslint-config는 바로 Rush Stack 내 하나의 구성요소로 제공되는 설정 프리셋입니다. 이런 태생으로 인해서 우리 회사의 개발 환경과 잘 맞는 부분이 많았습니다.

  • TypeScript 환경을 기반으로 합니다
  • 모노레포 환경이 고려되어 있습니다
  • 대규모 팀에서 실제로 사용하고 있습니다
  • Prettier와 함께 사용하도록 디자인되었습니다
module.exports = {
  plugins: ['import', 'no-relative-import-paths'],
  extends: [
    '@rushstack/eslint-config/profile/web-app',
    'plugin:storybook/recommended',
    'plugin:cypress/recommended',
  ],
  rules: {
    // 근거: 타입 추론으로 충분한 곳에 타이핑을 강요함
    '@rushstack/typedef-var': 'off',
    // 근거: React 컴포넌트의 경우 17 이하에서는 `undefined`가 아닌
    //      `null`을 리턴할 수 있기 때문에 사용하지 않음
    '@rushstack/no-new-null': 'off',
    // 근거: 상황에 따라 리턴 타입을 타입 추론에 맡기는 것이 나을수도 있음
    '@typescript-eslint/explicit-function-return-type': 'off',
    // 근거: 문서에 의하면 클래스를 많이 사용하는 프로젝트에서 사용할 수 있으나,
    //       팀 내 개발 패턴은 함수형을 지향하므로 불필요함
    '@typescript-eslint/explicit-member-accessibility': 'off',
    // 근거: useEffect 안에서 await 사용 불가
    '@typescript-eslint/no-floating-promises': 'off',
    ...,
  },
  ...,
};

사용 중인 ESLint 설정의 일부입니다

라이브러리와 프레임워크

회사 내에서도 어떤 사람이 개발하였는지, 언제 개발하였는지에 따라 사용한 라이브러리와 프레임워크가 달라질 수 있습니다. 어떤 것을 사용하든 결과물을 만들어내는 데는 문제가 없지만, 협업에서는 큰 걸림돌이 됩니다. 시장에서 주로 사용하는 라이브러리를 사용하는 것이 인재 채용에 도움이 되며, 매번 다른 라이브러리를 학습하는 것은 불필요하게 시간을 낭비하게 됩니다.

가장 널리 사용되는 React를 기반으로 각 기능에 대해서 어떤 라이브러리를 사용할지 논의하였습니다. 효율적으로 조사하기 위해 팀원이 각각 특정 기능을 맡아 여러 라이브러리를 조사하고, 최종적으로 투표를 진행하여 결정하였습니다. 결정하는 기준은 주로 다음과 같았습니다.

  • Active하게 개발되고 있는지
  • 널리 사용되고 있는지
  • 기존 프로덕트에 비추어 보았을 때, 필요하는 기능을 모두 제공하는지


Wiki에 조사한 내용을 정리했습니다


npm trends를 살펴보며 많이 사용되고 active하게 개발되고 있는지 파악했습니다

회사에서 개발하다 보면 기술 스택을 보수적으로 정하는 경우가 더러 있지만, 위 기준에 맞는다면 새로운 라이브러리도 거리낌 없이 선택했습니다. 예를 들어, 기존에는 상태 관리 라이브러리로 MobX를 사용했는데, boilerplate 코드가 적고 사용자 수가 급격하게 늘고 있는 zustand를 새로운 표준으로 정했습니다. 또한 빌드에 사용되는 런타임을 Node 20으로 지정하였습니다. 논의했던 항목 중 일부에 대해 소개해 드리겠습니다.

빌드 프레임워크

기존에는 create-react-app(이하 CRA)을 사용하고 있었습니다. CRA는 초기 설정이 아주 간편하다는 장점이 있지만 추가적인 설정을 하기 너무 복잡합니다. 또한 번들러는 webpack을 사용하고 있는데, 코드 베이스가 조금만 커져도 빌드가 느렸습니다. CRA를 관리하고 있는 Facebook에서도 CRA를 deprecate하는 것을 고려하고 있다고 하여 변경이 필요하다고 의견이 모였습니다.

새로운 개발 환경에서는 최근 빠른 빌드 속도로 각광받고 있는 Vite를 사용하기로 결정했습니다. webpack 대신 esbuild, swc, rollup을 사용하여 빌드 속도가 굉장히 빨라졌습니다. Vite의 config 파일을 수정하여 필요한 커스터마이징을 할 수도 있습니다.

상태 관리

기존에는 MobX를 사용하고 있었습니다. 데이터 클래스를 정의하고 React 컴포넌트에 연결만 해주면 알아서 변경점을 감지하고 다시 렌더해주는 편리함을 제공해 주지만, 무거운 라이브러리이기 때문에 번들 크기가 커지게 되어 성능에 악영향을 줍니다.

새로운 개발 환경에서는 zustand를 사용합니다. 라이브러리 크기가 작아서 성능적으로 유리하며, boilerplate 코드를 최소화하여 간편하게 사용할 수 있습니다. 또한 redux를 제외한 상태 관리 라이브러리 중에서 성장세가 가장 가파른 라이브러리입니다.

팀에서 어떻게 react-query와 zustand를 쓰고 있는지 궁금하시다면 같은 팀 배민근 님의 글과 우아콘 발표를 참고하세요.

네트워크 모킹

기존에는 axios-mock-adapter를 사용하고 있었습니다. 네트워크 요청을 위해 사용하고 있던 axios에 바로 붙여 사용할 수 있다는 점이 간편했습니다. 하지만 axios에 의존적이고, 실제로 네트워크 요청이 발생하지 않아 디버깅이 불편하다는 단점이 있습니다.

새로운 개발 환경에서는 MSW(Mock Service Worker)를 사용합니다. 특정 네트워크 요청 라이브러리에 종속적이지 않고, service worker를 사용하기 때문에 브라우저 개발자 도구의 네트워크 디버깅 기능에도 네트워크 요청이 모두 기록됩니다.

팀에서 MSW를 어떤 방식으로 활용하고 있는지 더 자세히 알고 싶으시다면 같은 팀 류현승 님의 우아콘 발표를 참고하세요.

CSS 스타일링

기존에는 Styled Components를 사용했습니다. React와 함께 사용하기 편리하고, 스타일과 비즈니스 로직을 분리할 수 있다는 점이 편리했습니다. 그러나 반복적으로 스타일링을 위한 컴포넌트를 선언해야 하고 CSS 파일의 크기가 커진다는 단점이 있었습니다.

새로운 개발 환경에서는 Tailwind CSS를 사용합니다. CSS를 직접 작성하지 않고 필요한 스타일을 모두 적용할 수 있으며, 불필요한 클래스는 자동으로 제거되기 때문에 CSS 파일의 크기를 최적화할 수 있습니다.

개발 패턴

같은 라이브러리를 쓰고 코드 스타일을 통일하더라도, 프레임워크와 라이브러리를 사용하는 방법은 사람마다 제각각일 수 있습니다. 이렇게 되면 다른 사람이 봤을 때 로직을 이해하기 어렵습니다. 패턴화할 수 있는 로직이 있다면 비슷한 코드가 나올 수 있도록 하여 이해하기 쉬운 코드를 만들 수 있습니다.

class ClassComponent extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

const FunctionComponent = ({ name }) => (<h1>Hello, {name}</h1>);

코드 스타일이 통일되어 있더라도 동일 기능을 구현하는 방법은 여러 가지 일 수 있습니다.

범위가 넓고 모호한 부분이 많아서 논의 과정에서도 우여곡절을 많이 겪었습니다. 개발할 때마다 요구사항이 다르고 그에 최적화된 패턴이 달라질 수 있기 때문에 전체적으로 느슨하게 패턴을 정했습니다. 디렉토리 및 파일 구성은 어떻게 할 것인지, 위에서 정한 라이브러리는 어떤 방식으로 사용할 것인지 위주로 표준을 정리했습니다.

아래는 논의했던 개발 패턴을 정리한 문서들의 일부입니다.

  • Store 작성 방법
  • React-query 개발 가이드
  • API, Service, Mock 작성 가이드
  • react-hook-form을 사용한 Form 개발 가이드
  • 단위 테스트 및 E2E 테스트 작성 가이드
  • i18n 메시지 관리 가이드

문서도 중요하지만 우리는 개발자이기 때문에 문서를 보는 것보다 코드를 보는 편이 훨씬 이해가 빠릅니다. 그래서 개발 패턴을 가시화하기 위해 스캐폴딩 레포지토리를 만들었습니다. 레포지토리 내에 아주 간단한 todo 기능을 만들어 개발 패턴을 표현했습니다.

스캐폴딩 레포지토리는 표준에서 정한 개발 패턴을 보여주는 목적도 있지만, 새로운 프로젝트를 시작하기 위해 레포지토리를 추가하는 경우 번거로운 셋업 과정 없이 바로 개발에 들어갈 수 있도록 하는 개발 경험 향상의 목적도 있었습니다.


스캐폴딩 레포지토리의 구조입니다

배포

열심히 개발하고 테스트까지 마치면 배포 과정만 남게 됩니다. 앞에서 정했던 부분들에 비하면 간단한 부분이니 굳이 따로 정할 필요가 없을 거라고 생각할 수 있지만, 배포 내용을 관련 부서와 공유하고 실수 없이 운영 환경에 배포하려면 정확한 절차를 정해두어야 합니다. 또한, 개발 환경에서는 개발 경험을 향상시키기 위한 기능이, 운영 환경에서는 더욱 안전하게 배포할 수 있는 기능이 필요합니다.

개발 환경은 기존에 하나에 한 가지 형상만을 배포하여 사용하고 있었는데, 다른 배포 일자를 가진 여러 가지 기능을 동시에 개발하는 경우 운영에 배포될 형상과 테스트 형상이 달라질 수 있는 점, 그리고 특정 상황에서 하나를 제외한 나머지 기능의 테스트를 중단해야 하는 불편한 점이 있었습니다. 이 부분은 여러 가지 개발 형상을 둘 수 있도록 기능을 개발하여 해결했습니다. 테스트하는 쪽에서는 운영에 배포될 형상을 확인하기 때문에 안정성이 대폭 증가하였습니다.

여러가지 개발 형상을 두는 방법은 다양하게 구현할 수 있지만, 회사의 공통 플랫폼을 사용하고 기존 구조를 크게 바꾸지 않아도 빠르고 쉽게 적용할 수 있는 방법을 고민했습니다. 이때 떠오른 것이 A/B 테스트 플랫폼인데요. 우리 회사에는 실험플랫폼이라는 A/B 테스트 플랫폼이 있습니다.

실험플랫폼에서는 여러 기준으로 그룹을 분배하여 결과를 돌려주는 기능을 합니다. 이것을 이용해 배포 파이프라인을 실행할 때 어떤 그룹에 배포할지 결정하고, S3 버킷에 디렉토리로 구분하여 저장했습니다. 그리고 index.html 파일에는 원래의 코드 대신 실험플랫폼 API를 호출하고 그에 맞는 JS를 불러와 HTML에 삽입하는 기능을 구현했습니다.

실험플랫폼의 그룹 분배 기능을 이용하여 어떤 베타 환경을 선택해야 하는지 결정하고, 그에 맞는 JS 파일을 불러오는 흐름으로 진행됩니다.


실험플랫폼에서 그룹을 분배받아 알맞는 JS를 불러옵니다

운영 환경에서는 사전 배포 기능을 사용하고 있습니다.

이전에 운영 환경에서만 확인할 수 있는 변경을 진행했는데, 테스트 과정에서 확인되지 못하고 운영 환경에 배포되었다가 몇 분 동안 주문내역, 장바구니 등 팀에서 관리하는 지면을 이용할 수 없게 되었던 적이 있습니다. 이런 문제를 방지하기 위해 서버의 Blue/Green Switch 배포 방식과 비슷하게 신규 형상을 사전에 배포해두고 모든 사용자에게 적용하기 전에 내부에서 간단한 테스트를 진행할 수 있도록 했습니다.

마무리

이런 점이 좋았어요

팀 내 모든 파트에서 동일한 컨벤션과 아키텍처를 사용하게 되어서 파트 간 업무 로테이션을 더욱 원활하게 할 수 있게 되었습니다. 업무 도메인만 파악하면 코드를 작성하는 방식은 거의 차이가 없기 때문에 다른 업무에도 빠르게 참여할 수 있습니다. 최근에 팀 내에 신규 파트가 생겼는데, 업무 지원을 위해 다른 파트의 코드를 작업할 때 이전보다 적응하는 시간이 줄어들고 코드 결과물의 품질이 좋아졌습니다.

신규 입사자가 들어오게 되었을 때 온보딩에 대한 걱정이 줄었습니다. 어떤 업무를 하는지 설명하는 것도 중요하지만 우리가 어떻게 개발을 하고 있는지 알려주고 적응하게 하는 것도 중요합니다. 표준 개발 환경을 개선하면서 다양한 문서와 코드 예제를 만들어 두었기 때문에 새로 오신 분도 쉽게 적응하실 수 있을 것 같습니다.

무엇보다도 팀원들과 기술적인 논의를 장시간 심도 있게 할 수 있었다는 점이 제일 좋았습니다. 대부분의 시간을 업무에 쫓겨 순수한 기술 논의는 할 기회가 생각보다 적었는데, 다양한 프론트엔드 기술을 함께 살펴보고 연구하며 더 나은 개발 환경을 위해 노력했다는 점에서 나와 팀의 성장 모두 잡을 수 있었습니다.

이런 걸 더 해보면 좋을 것 같아요

성능과 관련된 내용을 추가하면 좋을 것 같습니다. 이번 개선에서는 개발 환경에 대한 내용이 대부분이었는데, 개발한 제품이 많은 사용자들의 기기에서 빠르게 작동하는 것도 중요합니다. 성능을 측정하는 방법을 가이드로 작성하고 권장 성능 지표를 설정해 보면 도움이 될 것 같습니다.

단위 테스트와 E2E 테스트 가이드는 이번 개선에 준비되었지만, UI 디자인 변경에 대한 테스트는 아쉽게도 대비하지 못했습니다.

시각적 회귀 테스트를 적용하게 된다면 비즈니스 로직뿐만 아니라 UI 변경에 대해서도 안정성을 끌어올릴 수 있을 것 같습니다.


  1. AST: Abstract Syntax Tree. 컴파일러/인터프리터가 구문 분석을 거친 후 만들어내는 결과물. 괄호, 세미콜론 등의 구분자 정보는 가지고 있지 않기 때문에 서로 다른 소스코드가 같은 AST를 가질 수 있다.