*FEConf2024에서 발표한 <바퀴 대신 로켓 만들기>를 정리한 글입니다. 발표 내용을 2회로 나누어 발행합니다. 1회에서는 개발자가 직면하는 ‘디자인 프로토타입’ 병목 현상과 그 해결 방법에 대해 알아봅니다. 2회에서는 ‘백엔드 API’, 그리고 또 다른 병목의 해결 방법에 대해 알아봅니다. 본문에 삽입된 이미지의 출처는 모두 이 콘텐츠와 같은 제목의 발표 자료로, 따로 출처를 표기하지 않았습니다.‘바퀴 대신 로켓 만들기’양의현 토스페이먼츠 개발자 바퀴 대신 로켓 만들기 (1)바퀴 대신 로켓 만들기 (2) 이전 글에서는 토스페이먼츠 프론트엔드 개발자가 레거시 문제를 해결하다 만난 병목, 그 가운데 ‘디자인 프로토타입에 대한 병목’과 이를 해결한 내용, 나아가 ‘서버 API 병목’ 문제에 대해서도 알아봤습니다. 이번 글에서는 이를 해결하고자 Codegen을 구현한 방법과 마지막 병목에 대해 다뤄보겠습니다. 토스 페이먼츠 codegen서버 API 병목을 해결하기 위해 제가 ‘직접 구현하겠다’고 말했던 OpenAPI Code Generator는 아래와 같은 역할을 해야 합니다. JSON 스키마를 원하는 형식 대로 다듬기타입 스크립트, zod 기반 코드로 구현하기파일로 쓰기 이렇게 구현하려는 Codegen에는 리소스(Resource)라는 개념이 있습니다. 여기에서 리소스란 하나의 파일에서 관리 가능한 API 명세의 객체라고 생각하면 됩니다. 이 리소스로 스펙을 관리하도록 계획했습니다. 이를 통해 API에 대한 정보가 소스 코드 곳곳에 퍼지는 현상을 방지하고자 했습니다. 그리고 필요에 따라서 리소스에 정의한 파라미터가 스키마 파서로 처리되어 정합성을 확인하도록 했으며, 리소스 이름을 기준으로 필요한 파라미터와 응답 데이터를 추론하는 기능을 제공했습니다. 이런 도구, 그리고 도구를 만드는 예시는 오픈 소스 생태계에 굉장히 많이 있습니다. 그런 만큼 누구나 도구들을 자유롭게 선택하여 쉽게 만들 수 있다고 생각합니다. 예를 들어 스웨거 처리를 위한 스웨거 파서, JSON 객체를 zod 객체로 변환하는 도구, 템플릿 사용을 위한 핸들바, CLI 처리를 위한 clipanion 등의 도구 등을 잘 조합하면 다양한 방법으로 코드 제네레이터를 만들 수 있습니다. Codegen 구현하기Codegen을 구현하는 과정을 자세하게 알아보겠습니다. 스웨거 독스 URL로부터 오픈 API 스펙 객체를 생성하는 것이 시작입니다. 이 스펙의 일부 요소는 spec.components 요소로부터 리퀘스트 바디와 리스폰스에 대한 정보를 가져와서 zod 스키마로 생성합니다. 그리고 spec.paths로부터 API 엔드 포인트를 비롯한 각종 스펙을 원하는 형태의 객체로 가공합니다. 마지막으로 이 결과물을 원하는 위치에 타입 스크립트 파일로 생성하는 작업을 진행합니다. 이때 spec.components란 무엇일까요? spec.components에서는 아래와 같이 스웨거의 스키마들 목록을 볼 수 있습니다. 스웨거에서 openapi.json이라는 json 스키마 객체를 뽑아 오고, 이 객체를 다듬어 자동으로 스키마 파일을 생성해 줍니다. spec.paths도 마찬가지로 스웨거에서 패스 목록을 확인할 수 있고, 이 데이터를 json으로 파싱하고, 파싱한 결과를 앞서 정의한 리소스(Resource) 형태로 가공해서 생성합니다. 실제로 코드를 실행하면, 커맨드를 입력하자 몇 초 만에 결과물이 생성되는 것을 볼 수 있습니다. Codegen의 효과Codegen은 저 혼자 일주일 정도 시간을 사용해 구현했습니다. 생각보다 짧은 시간 안에 구현했지만, 이로 얻은 여러 이점이 있습니다. 우선 CLI로 생성한 api 엔드 포인트는 2,000개가 넘으며, 이 엔드 포인트로부터 생성된 코드 라인 수는 3만 줄 넘는 부분도 있었습니다. 이런 방대한 양의 코드를 단 10여 초 만에 생성할 수 있도록 자동화 시킨 것입니다. 이 결과물들은 당연히 재사용이 가능한 형태로, 무엇보다 스펙을 제대로 옮겨왔는지에 대한 검증을 할 필요 없이 CLI 한 번만 실행하면 바로 동기화되는 자동화 루틴을 얻을 수 있었습니다. 두 번째 진화 : 토스 페이먼츠 CodegenCodegen을 도입함으로써 스펙 문서를 확인하고, 스펙을 코드로 생성하고, 메소드를 호출해서 문제점이 파악되면 스펙을 수정하는 과정의 반복을 효율적으로 바꿀 수 있었습니다. 즉, 개발자가 API 문서를 보고 한 땀 한 땀 직접 코드를 옮기는 과정이 필요 없어지고, CLI 명령어 한줄만 실행하도록 축소한 것입니다. 또한 API 오류가 발생했을 때, 옮기는 과정의 실수인지, 잘못된 서버 API 구현인지 추적할 필요가 없어졌습니다. 유즈케이스에 맞게 가공할 수 있는 100% 제어 가능한 도구를 얻었으며, API가 반환하는 인터페이스가 바뀌어도 변경 사항을 즉시 싱크할 수 있습니다. 이렇게 지금까지 백엔드 개발에 의한 의존성에서 벗어나는 방법에 대해 알아봤습니다. 3. 프론트엔드 개발의 비효율에 의한 병목사실 앞서 제시한 세 가지 병목에 따르면, 이번 단락은 ‘제품 요구 사항 분석’에 의한 병목을 알아봐야 하는 것이 맞습니다. 그러나 사실 제품 요구사항에 대한 의존성을 끊어내는 일을 기술적으로 해결하는 것이 쉽지는 않다고 판단했습니다. 그래서 세 번째 주제를 살짝 바꿔봤습니다. 앞선 2가지 병목과 같은 외적인 요소 말고, ‘프론트엔드 개발자의 프론트엔드 개발 자체에 비효율이 있지 않을까?’ 라는 생각으로 시작했습니다. 프론트엔드 개발 생태계에는 너무나 많은 라이브러리나 툴이 있고, 많은 개발자가 이를 활용해 개발합니다. 예를 들어 리액트, Next.js, Vite, Tanstack Query 등이 대표적입니다. 하지만 이 도구들을 활용하는 결과물은 개발자마다 모두 다를 것입니다. 저는 이렇게 서로 다른 결과물을 만들어내는 일 자체가 또 다른 ‘바퀴를 만드는 일’이라고 생각합니다. 즉, 이런 비효율을 반복하지 말고, 모두가 일관성 있고 지속 가능한 코드를 만들어낼 수 있는 기반이 필요하다고 생각했습니다. 토스 페이먼츠 프레임워크: Pramework이 기반을 만들기 위해, 하나의 방식으로 통일한 프레임워크를 만들어야 한다고 생각했습니다. 이 프레임워크를 만들기 위해서 리파인(Refine)이라는 도구를 활용했으며, 많은 부분에서 아이디어를 얻어 설계했습니다. 리파인은 오픈소스 리액트 어드민 툴입니다. 이 툴에서 제공하는 인터페이스들은 헤드리스한 형식으로 제공되기 때문에 특정 UI와 결합되어 있지 않습니다. 따라서 자유로운 조합을 사용할 수 있습니다. 또한, 리소스라는 개념을 사용해 API 명세를 한 곳에서 객체 형태로 관리할 수 있으며, 필요한 기능들은 프로바이더 패턴을 통해 자유롭게 주입할 수 있습니다. 저는 이러한 리파인의 아이디어를 바탕으로 여러 가지 비효율을 해결해 보고자 했습니다. 프론트엔드에서 어드민 제품을 만들 때 가장 많이 반복되는 비효율은 무엇일까요? 제가 생각한 비효율은 아래와 같습니다. 데이터 페칭폼 컨트롤테이블로깅 데이터 페칭첫 번째로 데이터 페칭부터 살펴보겠습니다. 먼저 API 스펙을 리소스 객체로 분리하고 API에 대한 맥락을 한곳에서 제어할 수 있게 했습니다. 그리고 리소스와 API 클라이언트는 리액트 컨텍스트를 통해서 프로바이더에 주입하고, 빈번한 CRUD 반복과 같은 ‘행위에 대한 훅’을 제공하여 데이터를 불러올 때 목적을 명확하게 파악할 수 있고, 선언적인 방식으로 사용할 수 있게 만들었습니다. 마지막으로 리소스 이름을 기준으로 필요한 요청 파라미터와 반환된 응답을 자동으로 추론할 수 있도록 했습니다. 코드로 좀 더 알아보겠습니다. 아래와 같이 useQuery와 직접 만든 useItem을 비교해 보면, useQuery는 코드가 굉장히 장황하고 API가 어떤 역할을 하는지 알기 어렵습니다. 프론트엔드 개발자가 어떤 파라미터에 어떤 데이터를 넘겨야 하는지 일일이 다 제어해야 합니다. 반면, useItem는 단일 객체를 가져오겠다는 의도가 명확하게 보입니다. 리소스를 주입하면 API 명세를 신경 쓰지 않습니다. 필요한 파라미터 객체만 넣어 보내도 헤더에 필요하다면 헤더에 배치를 하고, 쿼리에 필요하면 쿼리에 배치를 하는 등 내부적으로 필요한 부분에 적절하게 배치해서 처리합니다. 또한 단순히 코드만 간결해지는 것뿐만 아니라, 선언한 리소스 이름에 따라 필요한 파라미터를 자동으로 추론해 줍니다. 반환 값 역시 바로 추론하도록 만들어져있습니다. 폼 컨트롤다음으로 폼 컨트롤에 대한 개선입니다. 폼 컨트롤 역시 불필요한 비효율을 줄이기 위해 여러 가지 개선을 했습니다. 데이터 페칭과 마찬가지로 리소스에 정의한 API를 호출하고 호출한 결과는 디폴트 밸류에 자동으로 주입되도록 했습니다. 또, 쿼리 파라미터를 자동으로 파싱하여 디폴트 밸류로 주입하는 등 다양한 외부 소스로부터 폼 데이터 주입이 가능합니다. 마지막으로 폼을 제출하거나 초기화할 때 쿼리 파라미터를 자동으로 업데이트하는 기능도 만들었습니다. 이것 역시 코드로 더 알아보겠습니다. useForm이라는 기능은 리소스를 정의하면 폼을 렌더링 할 때 리소스 기반으로 GET 요청을 실행하여 기본 값을 주입합니다. 폼 데이터를 제출할 때도 저장 API를 호출할 수 있도록 submitter라는 메소드를 제공합니다. 이를 통해 원하는 시점에 자유롭게 호출하고 저장할 수 있게 했습니다. 그리고 쿼리 파라미터를 불러와서 파서를 필드 형태로 가공하거나, 제출할 때 필드를 다시 쿼리 파라미터로 변환하는 과정을 단일 구현체로 제공합니다. 따라서 사람마다 서로 다른 쿼리 파라미터에 대한 처리 방식을 고민할 필요가 없습니다. 테이블세 번째, 테이블에 대한 개선입니다. 테이블도 마찬가지로 리소스에 정의한 API를 호출해서 테이블 형태로 렌더링합니다. 테이블 특성상 트리구조 형태의 코드가 반복되기 쉬운 구조이기 때문에 깊은 depth의 JSX를 반복 작성하는 대신에, 모델이라는 개념을 주입해서 테이블이 렌더링 되도록 했습니다. 그 덕에 테이블 렌더링 코드의 가독성이 훨씬 좋아졌습니다. 마지막으로 테이블에 공통으로 필요한 요구사항인 페이지네이션, 열 번호, 체크 박스와 같은 요소를 공통으로 처리할 수 있게 했습니다. 아래 코드를 보면 반복되는 코드는 공통 옵션을 제공하고, 복잡하고 읽기 어려운 트리 구조에서 벗어나서 테이블이 어떤 역할을 하는지 명확하게 알 수 있습니다. 로깅마지막으로 로깅에 대한 개선입니다. 로깅 개선은 생각보다 간단합니다. 로그 클라이언트를 리액트 컨텍스트에 주입하고, 어떤 기능을 개발할 때 해당 컴포넌트를 Logger로 래핑 하면 자동으로 로깅을 실행하도록 했습니다. 개발자가 굳이 컴포넌트마다 로깅 처리를 했는지 신경 쓸 필요 없이 래핑만 해주면 알아서 로깅 처리를 할 수 있습니다. 세 번째 진화 : 토스페이먼츠 프레임워크이렇게 프레임워크로 4가지 비효율을 해결해 봤습니다. 이 프레임워크를 통해 여러 개발자가 서비스를 구현하더라도 일관성 있는 형태로 개발할 수 있게 됐습니다. 그와 함께 유지 보수가 가능한 형태의 서비스를 만들 수 있었습니다. 지금까지 디자인 프로토타입, 백엔드 개발, 프론트엔드 자체라는 3가지 병목을 해결하는 과정에 대해 소개했습니다. 이 노력을 통해 많은 것을 얻게 됐습니다. 시작할 때는 6개월 안에 이 기능을 개발하기 힘들 것이라고 생각했지만, 실제로는 2달 밖에 걸리지 않았습니다. 4개의 어드민과 400페이지 이상의 메뉴를 여러 개발자가 연차 상관없이 동일한 수준의 품질을 가진 코드로 개발할 수 있었습니다. 화면을 구현하는 데 하루 정도 걸리던 작업을 한 시간으로 줄인 경험도 있습니다. 코드들 모두 유지 보수가 쉽고 확장 가능한 구조로 만들어졌습니다. 마치며지금까지 다양한 비효율을 개선하며 프론트엔드 개발을 효율적으로 바꾼 과정에 대해 알아봤습니다. 제가 소개한 비효율 제거 과정이 유의미한 변화를 가져왔다고 생각하시나요? 여러분들도 업무를 진행하면서 비효율적인 순간을 목격한다면 무시하지 말고, 작은 부분부터 차근차근 변화를 시도해 보면 좋겠습니다. 이런 작은 변화들이 모여 제가 경험한 유의미한 변화와 같은 값진 순간으로 돌아올 것이라고 생각합니다. 감사합니다. ©️요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.