뒤로가기
개발에서 파레토 법칙이 작동하는 순간들

March 9, 2020

productivitycareer

파레토 법칙을 설명하는 글은 쓰지 않겠다. 검색하면 수천 개 나온다. 대신 이 글은 내가 직접 숫자를 뒤져보고 "진짜 이러네"하고 놀란 순간들의 기록이다. 이론이 아니라 데이터다.

버그 트래커를 열었더니#

시작은 팀 리드의 한마디였다. 작년 결제 시스템 리뉴얼 프로젝트가 끝나고 스프린트 회고 때, 팀 리드가 말했다. "이번에 버그가 어디서 많이 나왔는지 한번 정리해봐. QA 리포트 보면 될 거야."

단순한 숙제라고 생각하고 Jira에서 버그 티켓을 전부 뽑았다. 총 34개. 하나하나 어느 모듈에서 발생한 건지 태깅했다.

결과를 스프레드시트에 정리했을 때, 숫자가 눈에 띄었다.

  • 결제 플로우 (카드 결제, 간편결제, 포인트 적용): 14개
  • 폼 유효성 검사 (카드 번호, CVC, 유효기간, 주소): 8개
  • 장바구니 상태 동기화: 3개
  • 상품 리스트 렌더링: 4개
  • 주문 내역: 2개
  • 기타 (헤더, 푸터, 404 등): 3개

결제 플로우 + 폼 유효성 검사 + 장바구니. 이 세 모듈에서 **25개, 전체의 73%**가 나왔다. 코드베이스에서 이 세 모듈이 차지하는 비중은 파일 수 기준으로 약 28%. 28%의 코드가 73%의 버그를 만들고 있었다.

왜 이 세 곳에 버그가 몰릴까? 공통점이 있었다. 상태 전이가 복잡한 모듈이다. 결제 플로우는 사용자 입력 -> 유효성 검증 -> API 호출 -> PG사 응답 -> 성공/실패 분기 -> 에러 복구까지, 상태 전이가 12가지가 넘는다. 폼 유효성 검사는 입력값의 경계값, 특수문자, 포맷 체크, 실시간 피드백, 포커스/블러 시점의 로직 분기까지 엣지 케이스가 셀 수 없다. 장바구니는 서버 상태와 클라이언트 상태의 동기화 문제.

반면에 상품 리스트는 데이터를 받아서 보여주는 게 전부다. 상태 전이가 거의 없다. 그러니 버그도 적다.

이 데이터를 스프린트 회고에서 공유했더니 팀 리드가 말했다. "그러면 다음 프로젝트에서는 이 세 모듈에 테스트를 더 두껍게 쓰자." 단순한 분석이었는데 팀의 테스트 전략이 바뀌었다.

테스트 전략이 바뀐 방법#

이 분석 전에는 테스트를 '고르게' 쓰고 있었다. 커버리지 80%를 목표로, 버튼 컴포넌트에도 테스트를 쓰고, 결제 로직에도 테스트를 쓰고, 같은 비중으로.

Kent C. Dodds가 쓴 글에서 비슷한 이야기를 읽은 적이 있다. 그가 만든 "Testing Trophy"라는 프레임워크가 있다. 전통적인 테스팅 피라미드(유닛 테스트 많이, E2E 테스트 적게)와 다르게, 통합 테스트(integration test)에 가장 많은 비중을 두라는 거다. 이유가 명쾌하다. "When you mock something, you're removing all confidence in the integration between what you're testing and what's being mocked." 뭔가를 목(mock)하는 순간, 테스트 대상과 목 사이의 통합에 대한 신뢰가 사라진다는 말이다.

그리고 또 하나. Dodds는 커버리지 100%를 추구하지 말라고 한다. "You get diminishing returns on your tests as the coverage increases much beyond 70%." 70%를 넘어가면 추가 테스트의 효율이 급격히 떨어진다는 거다. 버그가 안 나는 단순한 코드에까지 테스트를 쓰는 건, 파레토 법칙의 반대편에 시간을 쓰는 거다.

이걸 팀에 적용했다. 결제 플로우, 폼 유효성 검사, 장바구니 동기화 모듈에는 통합 테스트를 두껍게 썼다. 실제 사용자 시나리오를 기반으로, 상태 전이의 경로를 하나하나 테스트했다. 카드 결제 성공, 실패, 타임아웃, 네트워크 에러, 유효하지 않은 카드, 잔액 부족, 포인트 부족 등. 테스트 케이스가 40개가 넘었다.

반면에 상품 리스트, 헤더, 푸터 같은 단순한 컴포넌트는 스냅샷 테스트나 Storybook 시각적 확인 정도로 가볍게 갔다.

결과? 다음 프로젝트(작년 하반기)의 QA 기간에 결제 관련 버그가 14개에서 3개로 줄었다. 같은 수준의 복잡도를 가진 프로젝트였는데, 테스트를 '어디에' 집중했느냐가 달라진 거다. 전체 테스트 작성 시간은 비슷했다. 분배만 바꿨다.

GA 데이터를 열었더니#

두 번째 발견은 프로덕트 쪽이다.

작년에 어드민 대시보드를 만들 때, 기획서에 기능이 15개 있었다. 실시간 매출 현황, 주문 검색, 환불 처리, 일별/주별/월별 통계, 사용자 관리, 상품 등록/수정, 쿠폰 관리, 리뷰 관리, 배송 추적, 알림 설정, 정산 내역, 리포트 다운로드, 로그 조회, 권한 설정, 시스템 설정. 기획서만 50페이지.

2개월에 걸쳐 전부 만들었다. 디자인대로, 기획서대로. 배포하고 한 달 뒤에 GA 데이터를 처음으로 뽑아봤다.

사용자가 매일 쓰는 기능은 2개였다. 실시간 매출 현황과 주문 검색. 이 두 페이지의 일일 활성 사용자 수가 전체의 **61%**를 차지했다. 환불 처리와 일별 통계를 포함하면 4개 기능이 전체 사용량의 89%.

나머지 11개 기능의 사용량을 보면서 좀 허탈했다. 시스템 설정 페이지는 한 달 동안 접속한 사람이 3명이었다. 초기 설정할 때만 들어간 거겠지. 리포트 다운로드는 월 7회. 로그 조회는 월 12회. 내가 권한 설정 페이지를 만드는 데 3일을 쓴 건데, 그 페이지를 월 5명이 쓴다.

Addy Osmani가 웹 퍼포먼스에 대해 쓴 글에서 비슷한 프레임을 본 적이 있다. 퍼포먼스 버짓(performance budget) 이야기인데, 핵심은 이거다. "Deciding a page can't exceed 500kB when mockups contain three carousels and high-resolution images isn't effective." 모든 페이지를 동일하게 최적화하는 게 아니라, 사용자에게 중요한 경로(critical path)에 집중하라는 거다. 비핵심 기능은 크리티컬 패스에서 아예 빼버리는 게, 전체를 중간 정도로 최적화하는 것보다 낫다.

어드민 대시보드에도 같은 원리가 적용된다. 15개 기능을 모두 동일한 퀄리티로 만드는 것보다, 매출 현황과 주문 검색 2개를 정말 잘 만들었으면 더 큰 가치를 줬을 거다. 매출 현황 페이지의 로딩 속도를 0.5초 줄이는 게, 월 5명이 쓰는 권한 설정 페이지를 만드는 것보다 임팩트가 크다.

이 경험 이후로 기획 단계에서 질문을 하게 됐다. "이 기능을 쓸 사용자가 몇 명일까요?" "이게 없으면 사용자가 어떤 불편을 겪을까요?" PM이 싫어할 수 있는 질문이다. 기획서에 넣은 건 다 이유가 있을 테니까. 근데 이 질문이 때로는 우선순위를 바꾸기도 했다. "그러고 보니 이건 다음 분기에 넣어도 될 것 같네요."

번들 사이즈를 분석했더니#

세 번째 발견은 프론트엔드 퍼포먼스에서였다.

프로젝트의 초기 로딩 속도가 느려서 원인을 찾고 있었다. Webpack Bundle Analyzer를 돌렸다. 전체 번들 사이즈 1.2MB. 어디서 용량을 잡아먹고 있나?

트리맵을 보는 순간 바로 보였다. moment.js가 전체의 28%를 차지하고 있었다. 그 다음이 lodash 15%, 차트 라이브러리 12%. 이 세 개가 전체 번들의 55%.

moment.js는 날짜 포맷팅에 쓰고 있었는데, 실제로 사용하는 함수는 format()diff() 두 개뿐이었다. 두 개의 함수를 위해 전체 라이브러리(280KB gzipped)를 로딩하고 있었다. lodash도 마찬가지. debounce, throttle, cloneDeep 세 개를 쓰는데 전체를 임포트하고 있었다.

moment.js를 date-fns로 교체하고 (트리 셰이킹 가능), lodash를 개별 함수 임포트로 바꿨다. 차트 라이브러리는 dynamic import로 필요할 때만 로드하게 변경.

결과: 번들 사이즈 1.2MB -> 520KB. 57% 감소. 초기 로딩 시간 3.1초 -> 1.4초.

Osmani가 퍼포먼스 버짓 글에서 쓴 표현이 딱 맞는 상황이었다. "Performance budgets are not just thresholds. Much like a financial budget, they're something consciously spent." 퍼포먼스 버짓은 재정 예산과 같다. 한정된 용량을 '어디에 쓸 것인가'의 문제다. 280KB를 날짜 포맷팅 하나에 쓸 것인가, 아니면 사용자에게 의미 있는 기능에 쓸 것인가.

번들 분석에서도 80/20이 작동했다. 전체 의존성의 약 20%(3개 라이브러리)가 번들 사이즈의 55%를 차지하고 있었다. 이 3개만 최적화해도 전체 사이즈가 절반 이하로 줄었다. 나머지 수십 개의 라이브러리를 하나하나 최적화하는 것보다, 큰 3개를 손대는 게 압도적으로 효율적이었다.

디버깅의 패턴#

2년간의 프론트엔드 버그를 뜯어보면 또 비슷한 패턴이 나온다. 내가 겪은 버그의 대부분은 다섯 가지 카테고리에 속한다.

  1. 상태 관리 문제 - 불필요한 리렌더링, stale closure, useEffect 의존성 배열 누락
  2. 비동기 처리 문제 - race condition, 에러 핸들링 누락, 컴포넌트 언마운트 후 setState
  3. CSS 레이아웃 문제 - 반응형 깨짐, z-index 충돌, overflow
  4. API 응답 형식 불일치 - null/undefined 미처리, 옵셔널 필드 누락
  5. 브라우저 호환성 - Safari에서만 안 되는 것들

체감상 85% 이상이 이 다섯 가지 안에 든다. 특히 1번(상태 관리)과 2번(비동기 처리)이 전체의 절반 이상이다.

이걸 알면 디버깅이 빨라진다. 버그 리포트가 들어오면 전체 코드를 훑는 대신, "상태 문제인가?" -> "비동기 문제인가?" -> "CSS인가?" 순서로 체크한다. 범인 후보를 5명으로 줄이고 시작하면 수사가 빠르다.

Kent C. Dodds가 테스트에 대해 쓴 철학과 연결된다. "Write tests. Not too many. Mostly integration." 테스트를 쓰되, 너무 많이 쓰지 말고, 대부분 통합 테스트로. 여기서 "Not too many"가 핵심이다. 모든 경우를 테스트하는 건 불가능하고 비효율적이다. 버그가 집중되는 영역(상태 관리, 비동기 처리)의 통합 테스트를 두껍게 쓰는 게, 모든 함수에 유닛 테스트를 쓰는 것보다 실제 버그를 잡는 확률이 높다.

시간의 80/20#

가장 실용적인 발견은 시간에 대한 거다.

하루 동안 내 작업 로그를 분석해봤다. 가장 의미 있는 아웃풋이 나온 시간은 오전 10시~12시, 딱 2시간이었다. 이 시간에 핵심 코딩 작업의 대부분이 완성됐다. 나머지 6시간은 미팅, 코드 리뷰, 슬랙, Jira 정리, 자잘한 수정에 쓰였다. 중요하지 않은 일이라는 게 아니다. 근데 '핵심 가치를 만드는' 시간은 2시간이었다.

이걸 인식하면 그 2시간을 보호하게 된다. 아침에 가장 어려운 코딩 작업을 먼저 한다. 에너지가 높은 시간에 가장 중요한 일을 한다. 오후에 미팅이 몰려도 "오전에 핵심 작업은 끝냈으니까"라는 안정감이 있다.

코드 리뷰에서도 같은 원리를 쓴다. PR 전체를 동일한 강도로 리뷰하지 않는다. 비즈니스 로직이 들어있는 파일, 상태 관리가 복잡한 파일에 집중하고, 단순한 스타일 변경은 가볍게 본다. 리뷰 시간이 같아도 잡아내는 버그 수가 다르다.

사후 분석의 한계#

주의할 점이 있다. 이 글에서 나온 숫자들은 전부 '사후' 분석이다. 버그가 어디서 나올지, 어떤 기능이 많이 쓰일지, 어떤 라이브러리가 번들을 잡아먹을지는 미리 알기 어렵다.

GA 데이터는 배포 후에야 나온다. 버그 분석은 QA 후에야 가능하다. 번들 분석도 빌드 후에야 보인다. "어디에 집중해야 하는가?"에 대한 답은 대부분 사전이 아니라 사후에 명확해진다.

그래서 80/20 사고방식의 진짜 가치는 예측이 아니라 피드백 루프다. 만들고, 측정하고, 집중점을 재조정한다. 첫 번째 프로젝트에서는 어디에 버그가 몰리는지 몰랐다. 분석하고 나서야 알았다. 두 번째 프로젝트에서는 그 정보를 바탕으로 테스트를 다르게 분배했다. 세 번째에서는 더 정밀해질 거다.

"20%만 하면 되잖아"라는 식으로 나머지 80%를 무시하면 안 된다. 시스템 설정 페이지를 월 5명이 쓰더라도, 그 5명에게는 100%의 문제다. 저빈도 기능이라고 퀄리티를 낮추는 건 다른 이야기다. 핵심은 '같은 시간이 있을 때 어디에 먼저 쓸 것인가'의 우선순위 문제이지, '나머지는 안 해도 된다'는 뜻이 아니다.

2년 동안 개발을 하면서 느낀 건, 모든 곳에 동일한 에너지를 쏟는 건 불가능하고 비효율적이라는 거다. 어디에 힘을 더 쓸지 판단하는 것도 실력이다. 그리고 그 판단의 가장 좋은 재료는 직감이 아니라 데이터다. 버그 트래커를 열어봐라. GA를 분석해봐라. 번들 분석기를 돌려봐라. 숫자가 어디에 집중해야 하는지 알려준다.