벌써 디어코퍼레이션에 합류한지 6개월이 지나가고 있습니다. 2022년 5월에 입사한 이후에 조직의 제품 개발 생산성을 높이기 위해 고군분투하고 있습니다. 반년이라는 길지 않은 시간이었지만 개발 프로세스를 정비하고 효율을 높이기 위해 도입했던 절차와 아이디어들을 공유합니다.

(이 글은 회사 블로그에도 기고했습니다: https://blog.deering.co/cost-reducing/)


안녕하세요, 디어코퍼레이션 물류팀 개발리드 김명재입니다. 2022년 5월에 이 회사에 입사한 이후에 조직의 제품 개발 생산성을 높이기 위해 고군분투하고 있습니다. 5개월이라는 길지 않은 시간이었지만 개발 프로세스를 정비하고 효율을 높이기 위해 도입했던 절차와 아이디어들을 공유하고 싶습니다.

이전 글에서 Shape Up을 이야기 했습니다. Shape Up으로 적당히 추상화된 기획서가 나오면 구현팀은 이 기획서를 바탕으로 상세 기획을 작성하고 구현에 돌입합니다. 이 때 발생할 수 있는 비효율을 찾아 없애면 빠르게 움직일 수 있습니다. 아래는 숨은 비용을 찾아 없애기 위해 저희가 도입한, 그리고 도입하고 있는 프로세스들입니다.

API Specification을 확정하기 전까지 코딩 금지 link
toc

어떤 개발 방법론을 사용하든 상세 기획까지 확정한 직후에 해야 할 일은 API Specification(이하 API Spec)을 작성하고 백엔드와 프론트엔드가 합의하는 것입니다.

Request의 path, query parameter, request body, response의 status, response body를 확정하는 과정에서 백엔드 개발자는 머릿속으로 백엔드를 어떻게 구현할지 구체적으로 상상할 수 있습니다. 머릿속에 그려진 구조를 코드로 뽑아내기만 하면 됩니다. 코드를 뽑아내는 과정에서 분명 예상치 못한 문제를 발견하게 되겠지만, 이 문제들은 경험이 쌓이면서 점점 줄어듭니다.

프론트엔드 개발자는 API Spec을 확정하는 과정에서 어떤 화면에 어떤 값이 필요한지 확인할 수 있고, API Spec이 확정된 이후에는 백엔드에서 실제 API를 제공하기 전까지 가짜 데이터로 프론트엔드를 구현할 수 있습니다. API Spec문서가 프론트엔드와 백엔드의 의존성을 끊어줌으로써 빠른 개발이 가능합니다.

백엔드 API의 실제 구현을 완료하고 프론트엔드와 백엔드를 연동하는 과정에서 예상치 못한 문제는 당연히 발생할 수 있습니다. 하지만 API Spec을 꼼꼼하게 리뷰했다면 기술적으로 치명적인 문제는 발생하지 않습니다.

Shape Up은 상세 기획을 뒤로 미룸으로써 빠르게 움직입니다. API Spec을 확정하는 것은 실제 구현을 뒤로 미룸으로써 빠르게 움직입니다. 높은 추상화 수준을 유지하면 세부사항에 대한 고려를 뒤로 미룰 수 있고, 세부사항을 고려하지 않아도 발생하는 문제는 크고 중요한 문제이므로 중요한 부분에 먼저 리소스를 투입할 수 있습니다.

Shape Up 방법론을 도입하고 API Spec을 확정하기 전까지 코딩을 안하게 되면 개발자는 조바심이 날 수도 있습니다. 일하고 있는 것 같지 않고, 뭔가 느린 것 같고, 문서만 계속 작성하고... 하지만 코딩만이 프로그래밍이 아닙니다. 기획도 프로그래밍, 구체화 과정도 프로그래밍입니다. '프로그램'의 정의는 '작업 지시서', 코딩을 뒤로 미룸으로써 프로그램을 빨리 만들 수 있습니다.

물류팀은 현재 Product Market Fit을 찾아가는 과정에 있습니다. 비즈니스 초기라서 변경사항들이 매우 빠르게 발생합니다. API Spec을 확정하기 이전에 코딩부터 시절엔 상세 기획 바뀔 때마다 코드도 함께 바뀌어야 했기 때문에 프론트/백엔드를 막론하고 변경에 대한 비용이 커서 상대적으로 느리게 움직일 수밖에 없었습니다. 빠른 제품 개발을 위해 API스펙을 합의하기 전에 코딩부터 했지만 이 선택은 결국 야근으로 이어졌습니다. Shape Up을 도입하고 코딩하기 전에 API Spec을 확정하면서 변경사항을 빠르게 반영할 수 있게 되었습니다. 물류팀은 더 적은 시간으로 더 많은 가치를 전달할 수 있도록 개발 프로세스를 계속 개선해나가고 있습니다.

테스트 주도 개발: 캐리 정산 프로덕트 백엔드의 모든 기능엔 자동화된 테스트가 있다 link
toc

책 '순서 파괴'의 PR/FAQ는 '테스트 주도 개발'이라는 소프트웨어 개발 방법론과 공통점이 있습니다. 작업이 끝났을 때 만족해야 할 요구사항을 미리 구체적으로 생각해서 테스트를 작성하는 방식은 기존의 코드를 작성하는 방식을 거꾸로 뒤엎는 효과적인 개발 방식입니다. 확정한 API Spec은 그대로 백엔드의 자동화된 테스트(End to end test)가 됩니다. 이 테스트를 만족하도록 백엔드 애플리케이션을 구현하면 백엔드 작업은 끝납니다.

테스트는 구현 완료 이후에도 계속 남아있어서 이후에 어떤 기능이 추가되거나 변경사항이 발생했을 때도 기존의 기능이 잘 작동하는지 확인할 수 있는 회귀테스트로 사용합니다. 새로운 코드를 추가하든 기존 코드를 변경하든 먼저 테스트를 작성하거나 변경하면 안정적인 코드베이스를 유지할 수 있습니다.

QA는 시스템에서 아무 문제도 찾지 못해야 한다. 테스트를 돌릴 때마다 QA의 보고 내용은 모든 것이 요구 사항에 따라 동작한다는 것이어야 한다. QA가 문제를 찾는다면, 개발팀은 개발 프로세스에서 어디가 잘못되었는지 찾아내야 하며 다음번에는 QA가 아무것도 찾지 못하도록 고쳐야 한다.

Excerpt From
58쪽, 2장 왜 애자일인가, 클린 애자일
로버트 C. 마틴

자동화된 프론트엔드 테스트에 대한 고민 link
toc

백엔드에는 요구명세와 일치하는 테스트가 모든 기능에 있기 때문에 잘 작동하던 기능에 문제가 생기면 바로 알 수 있습니다. 하지만 아직 프론트엔드에는 테스트가 꼼꼼하게 작성되어있지는 않기 때문에 예상치 못한 곳(=기존에 잘 작동하던 곳)에서 발생하는 버그는 늦게 발견할 수밖에 없습니다.

어떻게 하면 자동화된 프론트엔드 테스트를 쉽게 작성할 수 있을까요? 아직 명확한 결론에 도달하지는 못했습니만, 다음의 3가지 방식의 테스트를 적재적소에 작성하는 방식으로 프론트엔드 테스트 작성 문화를 만들어가야겠다고 생각하고 있습니다.

  1. 순수한 UI 컴포넌트 (행동과 상태를 모두 외부에서 주입받는다)

  2. 스크린 리더를 활용한 UI 통합테스트

  3. 렌더링 결과를 픽셀 단위로 검증하는 테스트

1. 순수한 UI 컴포넌트 (행동과 상태를 모두 외부에서 주입받는다) link
toc

순수함수는 입력이 동일하다면 항상 같은 값을 내보내는 함수입니다. 계산만 하고 상태를 변경하지 않으면 순수함수라고 불릴 수 있는데, 자세한 내용은 이제는 함수형 프로그래밍을 공부해야 할 때 | Myeongjae Kim을 읽어보시면 됩니다.

리액트 세상에는 댄 아브라모프가 제안한 Presentational and Container Component 패턴이 유명합니다. 원문을 읽어보시면 이 패턴이 하도 유명해진 나머지 모든 상황에서 맹목적으로 Presentational Component와 Container Component를 나눠서 작성하는 상황에 대해서 댄 아브라모프가 한탄하고 있음을 알 수 있습니다(...). 리액트가 Hooks를 도입하면서 댄 아브라모프는 Presentational and Container Component가 달성하려고 했던 'UI와 복잡한 비즈니스로직의 분리'를 이 패턴의 도움을 받지 않고도 달성할 수 있다고 판단한 것 같습니다.

그렇다고 하더라도 저는 이 패턴이 쓸모가 없어졌다고 생각하지 않습니다. 관심사를 분리하는 것은 어느 프로그래밍 패러다임을 막론하고 권장하는 일이기 때문입니다. 2021년 네이버 D2에서 발표한 'React VAC(Value Asset Component)'라는 패턴(https://tv.naver.com/v/23162062 , https://wit.nts-corp.com/2021/08/11/6461 )은 Presentational and Container Component 패턴이 달성하려는 'UI와 비즈니스 로직의 분리'를 극단으로 추구했습니다. UI를 그리는 컴포넌트는 모든 행동(함수)와 상태(데이터)를 주입받습니다. 오직 UI만을 위한 리액트 컴포넌트는 순수함수와 유사한 특성을 가집니다. UI 컴포넌트가 직접 행동이나 상태를 정의하지 않고 외부효과를 직접 일으키지 않기 때문에 테스트 상황에서는 UI컴포넌트가 잘 작동하는지 확인할 수 있는 행동과 상태를, 실제 상황에서는 비즈니스 로직과 관련된 행동과 상태를 주입할 수 있습니다.

UI 컴포넌트와 비즈니스 로직을 분리해서 작성하는 행위는 어떤 코드에서 외부 의존성을 분리하는 것과 동일한 원리를 가집니다. 예를 들어 스크롤을 내리면 메뉴가 사라지고 스크롤을 올리면 메뉴가 나타나는 기능을 구현하기 위해서 w3schools.com은 아래의 코드를 제시합니다.

/* When the user scrolls down, hide the navbar. When the user scrolls up, show the navbar */
var prevScrollpos = window.pageYOffset;
window.onscroll = function() {
  var currentScrollPos = window.pageYOffset;
  if (prevScrollpos > currentScrollPos) {
    document.getElementById("navbar").style.top = "0";
  } else {
    document.getElementById("navbar").style.top = "-50px";
  }
  prevScrollpos = currentScrollPos;
}

이 코드의 외부의존성은 window와 document입니다. 이 두 객체는 브라우저에만 존재하기 때문에 node환경에서 이 코드를 실행하면 에러가 발생합니다.

행동과 상태는 어떤게 있을까요? 일단 첫 번째 줄에 prevScrollpos라는 변수가 보입니다. onscroll 이벤트가 발생하기 직전의 스크롤 위치를 기억하는 변수, 곧 상태입니다. 그리고 window.pageYOffset이라는 상태를 발견할 수 있는데, 이는 현재 스크롤 위치를 의미합니다.

이후엔 이전 스크롤 위치와 현재 스크롤 위치를 비교해서 navbar를 보여주거나 숨깁니다. 그리고 현재 스크롤 위치를 이전 스크롤 위치를 의미하는 변수에 할당합니다. 총 3가지의 행동이 있군요. 행동과 상태를 정리해보면:

  • 행동
    1. navbar 보여주기
    2. navbar 숨기기
    3. 현재 스크롤 위치를 이전 스크롤 위치를 의미하는 변수에 할당하기
  • 상태
    1. 이전 스크롤 위치
    2. 현재 스크롤 위치

상태를 조금 더 생각해보면, 우리는 상태를 담을 변수가 필요한게 아니라 상태를 조회할 수 있는 행동이 필요한 것입니다. '스크롤 위치'라는 상태 대신 '스크롤 위치 조회'라는 행동을 제공해도 동일한 작동을 할 수 있습니다. 이는 객체지향 프로그래밍에서 객체의 변수를 직접 조회하는게 아니라 getter를 사용해서 변수의 값을 조회하는 것과 동일합니다.

결국 window와 document 객체를 통해 하고 싶은 행동은 아래 5가지입니다.

  1. navbar 보여주기
  2. navbar 숨기기
  3. 현재 스크롤 위치를 이전 스크롤 위치를 의미하는 변수에 할당하기
  4. 이전 스크롤 위치 조회하기
  5. 현재 스크롤 위치 조회하기

이 5가지 행동을 매개변수로 받아서 스크롤을 내리면 navbar를 숨기고 스크롤을 올리면 navbar가 나타나게 하면서도 외부 의존성이 없는 함수를 리턴하는 함수를 아래처럼 작성했습니다.

type HeaderAnimationHandlerArgs = {
  display: () => void;
  hide: () => void;
  setPrevScrollPos: (pos: number) => void;
  getPrevScrollPos: () => number;
  getCurrentScrollPos: () => number;
};

const createHeaderAnimationHandler: (
  args: HeaderAnimationHandlerArgs
) => () => void = ({
  display,
  hide,
  setPrevScrollPos,
  getPrevScrollPos,
  getCurrentScrollPos,
}) => {
  return () => {
    if (getPrevScrollPos() > getCurrentScrollPos()) {
      display();
    } else {
      hide();
    }
    setPrevScrollPos(getCurrentScrollPos());
  };
};

이 함수는 더이상 window와 document 객체를 의존하지 않기 때문에 특별한 mocking 없이도 node환경에서 실행할 수 있게됩니다. jest에서 제공하는 강력한 mocking을 사용하면 거의 모든 것을 테스트할 수 있지만 mocking을 위한 코드를 마냥 쉽게 작성할 수는 없습니다. 처음부터 외부 의존성과 비즈니스 로직을 분리해서 코드를 작성한다면 테스트를 수월하게 작성할 수 있게 됩니다. 

리액트 컴포넌트를 작성할 때는 Container Component에서 비즈니스 로직과 의존성 연동을 담당하고 Presentational Component는 비즈니스 로직이 담긴 함수를 주입받는 방식을 사용해서 유사한 효과를 거둘 수 있습니다.

2. 스크린 리더를 활용한 UI 통합테스트 link
toc

백엔드 테스트에서 API Request, Response를 검증해서 테스트하는 것처럼 프론트엔드에서는 접근성 관련 속성으로 입출력을 검증해서 테스트를 하면 좋습니다.

3. 렌더링 결과를 픽셀 단위로 검증하는 테스트 link
toc

  • Cypress: Headless 브라우저에서 실제로 렌더링해서 스크린샷 찍어놓고 비교하기
  • Chromatic: Storybook기반, 개별 컴포넌트의 변경사항을 픽셀 단위로 비교할 수 있습니다. https://www.chromatic.com

Trunk Based Development: 작은 단위의 변경사항을 자주 배포한다 link
toc

Trunk Based Development는 디어에 입사한 날부터(5월 2일)부터 회사 메신저에 여러 번 이야기했던 것 같습니다. Trunk Based Development는 작업 내용을 작은 단위로 잘라서 조그마한 변경사항을 Trunk라고 불리는 중앙화된 코드베이스에 자주 반영하는 방식입니다. 중앙화된 코드베이스에 변경사항을 반영할 때 이전에 작성한 테스트들이 모두 통과하는지 검증하는 단계를 추가해서 코드베이스를 보호할 수 있습니다.

프로그래머가 테스트를 돌린다. 모든 코드가 테스트를 통과하는지 확인하는 것은 프로그래머 담당이다. 그러니 당연히 프로그래머가 테스트를 돌려야 한다. 프로그래머가 테스트를 돌려보는 것이 스토리 완료 여부를 알 수 있는 유일한 방법이다.

프로그래머는 지속적 빌드Continuous Build 서버를 구축해서 이 과정을 자동화할 것이다. 빌드 서버는 프로그래머가 모듈을 체크인할 때마다 시스템의 모든 테스트를 수행한다. 모든 단위 테스트와 인수 테스트를 죄다 수행한다는 말이다.

Excerpt From
104쪽, 3장 비즈니스 실천 방법, 클린 애자일
로버트 C. 마틴

지속적 빌드는 절대 깨지지 않아야 한다. 체크인 전에 모든 인수 테스트와 단위 테스트를 수행해 봐야 한다. 당연하다! 만약 빌드가 깨졌다면, 굉장히 놀라운 일이 일어난 것이다.

Excerpt From
124쪽, 4장 팀 실천 방법, 클린 애자일
로버트 C. 마틴

서비스 개발에서 Trunk Based Development는 감히 필수라고 얘기하고 싶습니다. 코드베이스에 반영해야 하는 변경사항의 크기가 커질수록 코드 리뷰도 어려워지고 conflict가 발생했을 때 해소하기도 어려워지고 실제 배포했을 때 문제가 생기면 어떤 부분에서 문제가 생겼는지 추적하기도 어렵습니다. 변경사항의 크기가 커졌을 때 어떤 장점이 있는지 도저히 모르겠습니다.

다시 한 번 말하지만, 지속적 빌드는 절대 깨지지 않아야 한다. 깨진 빌드는 윤전기를 멈춰야 하는 사건이다. 사이렌이 울리면 좋겠다. CEO 사무실에 커다란 경광등이 켜져도 좋겠다. 깨진 빌드는 대박 큰 사건이다. 모든 프로그래머가 하던 일을 멈추고 빌드를 살펴보아야 한다. 그래서 다시 통과하게 만들어야 한다. 팀의 슬로건을 '빌드는 절대 깨지지 않는다'로 만들어야 한다.

Excerpt From
125쪽, 4장 팀 실천 방법, 클린 애자일
로버트 C. 마틴

CI/CD 설정은 프로젝트 시작부터 하는게 좋습니다, 어차피 언젠가 하려고 했던거라면요. 'Hello, world!'부터 CI/CD를 붙여서 점진적으로 개선하고 배포하는 것이 여러 의존성을 가진 큰 애플리케이션의 CI/CD를 설정하는 것보다 훨씬 빠르고 즐겁습니다.

코드 리뷰 link
toc

물류팀에 아직 코드 리뷰 문화를 도입하지 못했지만 꼭 리뷰 문화를 정착시키고 싶습니다. 리뷰를 받을 때까지 기다리는 시간이 개발 속도를 늦추는 것처럼 느껴질 수 있지만, 리뷰를 함으로써 버그를 찾아냈을 때 절약할 수 있는 시간을 고려해보면 그렇게 느린 것도 아닙니다.

PR의 크기는 작을수록 리뷰하기 좋습니다. merge하기 전에 다음 작업을 하고 싶다면 마지막 커밋에서 임시 브랜치를 파서 작업하고, PR을 merge한 이후에 trunk branch에서 새로운 브랜치를 만들어 임시 브랜치의 커밋을 cherry-pick 하면 됩니다. 이렇게 하면 리뷰와 상관없이 작업을 이어나갈 수 있습니다. 코드 리뷰를 하면서 컨벤션에 익숙해지고 서로의 코딩 스타일이 점점 닮아갈수록 리뷰에 필요한 시간은 줄어듭니다.

물류팀은 이상의 절차들을 도입했고, 앞으로 도입할 계획에 있습니다. 더 짧은 시간에 더 많은 가치를 고객에게 전달할 수 있도록 물류팀은 최선을 다해 노력하고 성장할 것입니다. 끝.