안정적인 SPA 리팩터링을 위한 준비 전략


필자가 꽤 오래 몸담고 있던 프로젝트의 지원 업무가 종료되어 몇 달 전 새로운 SPA 프로젝트에 투입되었다. 필자의 팀원들은 필자보다 몇 달 먼저 이 신규 프로젝트에 편입되어 인수인계를 마치고 개발과 운영을 진행하는 중이었다. 필자와 팀이 새로운 프로젝트의 구조를 보다 명확하게 설계하고 요구사항의 변화에 더욱 기민한 코드로 리팩터링 하기 위해 고민했던 팀의 개발 전략과, 그 접근 방식으로 개발을 어떻게 진행할 것인지 공유하고자 한다.

초기 상태

팀에서 인수받은 프로젝트는 프런트엔드 개발 주체가 몇 차례 바뀌었고, 많지 않은 인력으로 유지 보수와 신규 기능 개발을 동시에 진행하고있는 SPA 프로젝트다. 그러다 보니, 같은 목적을 달성하기 위해 사용된 서로 다른 방법론이 각자의 방법으로 작성되었고 하나의 컨벤션으로 통일되지 않고 남아있다. 이것이 필자가 코드를 파악하면서 '레거시 코드가 남아있다'는 느낌을 받은 이유라고 생각한다. 이런 통일되지 않은 코드들은 필자의 팀 코딩 컨벤션에도 부합하지 않아서 변경이 필요하다.

UI와 상태를 관리하고 사이드이펙트를 주는 로직 모두 테스트 코드가 없어서 기존 기능 수정이나 확장에 대한 리스크 관리가 어렵기도 했다. 지금은 팀 인원이 5명이 되었지만, 인수 초기 프로젝트 멤버는 2명이 전부여서 부담이 더 컸다.

인수인계의 어려움

이렇게 컨벤션이 제각각인 코드는 신규 인력이 코드를 이해하고 프로젝트 설계 구조에 적응하는데 시간이 오래 걸리게 만드는 걸림돌이 된다. 문장 끝에 세미콜론을 사용하지 않도록 바꾸거나 코딩 스타일이 다른 부분은 ESLint와 Prettier를 적용하여 대부분 정리할 수 있었다. ESLint 규칙을 적용하면서 상당히 많은 코드 변경 사항이 발생했지만, 이는 팀원끼리 코드 리뷰를 통해 서로 확인해보기만 하면 될 정도의 수정이라 생각한다. 다만, 코드 양이 많아 단순 노동에 들어가는 시간이 많았다.

그리고 단순 코딩 컨벤션을 넘어서 애플리케이션 설계가 일관되지 않은 경우, 컴포넌트의 기능을 변경했을때 영향받는 범위를 특정하기 어려울 수 있다. 예를 들어, 특정 컴포넌트에서는 불필요하다고 판단되어 실수로 지운 어떤 메서드가 사실은 부모 컴포넌트에서 직접 호출하는 메서드인 경우도 있을 것이다. 이런 이유때문에 매번 서비스 Full QA를 받아야 한다면 QA 리소스가 과도하게 소모되어 짧은 배포주기로 배포하기 어려울수도 있다.

전략을 세우기 위한 문제 분석 과정

코딩 스타일과 개발 컨벤션이 달라서 겪었던 불편함 때문에 타오른 불씨가 프로젝트 설계를 개선하는 리팩터링을 하고 싶다는 열망으로 번지면서 팀원 모두의 프로젝트 개선 의욕을 끌어올렸다. 물론 당장 당장 제품을 운영하고 신규 기능을 추가하는데 큰 문제는 없었다. 다만, 팀원 모두 기존에 했던 것과 다른 분야의 프로젝트를 맡으면서 의욕이 가득찬 상태였고, 이 에너지를 양분 삼아 어떻게 프로젝트에 새롭게 변화를 주고 싶은지 며칠에 걸쳐 의견을 교환했다. 개발이 시작된 지 오랜 시간이 지난 프로젝트인 만큼, 프런트엔드 프레임워크나 라이브러리 전면 변경의 가능성도 열어두고 의견을 취합했다.

회고

어떤 활동을 하더라도 회고는 정말 중요하다고 생각한다. 정기적인 회고가 아닌 '뭔가 문제가 발생한 것 같은 느낌이 들 때' 하는 회고일 수록 서로의 의견을 잘 맞추고 문제를 명확히 파악할 수 있게 해준다. 팀원 모두가 이제 막 프로젝트 업무를 시작하는 단계인 만큼, 필자는 팀원 각자의 생각과 성향을 알고 싶었다. 가능하다면 그 성향과 다양한 아이디어들을 통해 팀원 모두가 뭔하는 방향을 조금씩 맞춰서 하나로 통일된 팀의 방향을 만들기 위해 총 3 단계의 회고를 고안했다.

  1. 원하는 개발 방향 부터 프로젝트 업무에 대한 서로의 생각을 나누고, 그에 따른 개발 컨벤션 정하기

    • 팀에서 각자 하고싶은 개발의 이상향 공유 (각자의 개발 성향 공유)
    • 통합 테스트 작성을 더욱 강력하게 만들 수 있는 방법 (과업의 최적화 방법 고민)
    • 앞으로 진행할 리팩터링에 대한 의견 (궁극적 목표에 대한 자유로운 의견 취합)
  2. 앞으로의 해야 할 과업들 목록으로 만들기
  3. 과업의 우선순위와 처리 순서 정하기

우선, 위 3 단계 중 1 단계를 진행했다.

Figjam 캔버스를 이용해서 의견 취합하며 구조화 해본 결과물

Figjam 캔버스를 이용해서 의견 취합하며 구조화 해본 결과물

하루에 걸친 회고를 통해 팀의 방향은 무엇이며, 우리 모두는 이 프로젝트를 어떤 모습으로 바꾸고 싶은지 팀원 모두가 명확하게 이해할 수 있었다.

  • 통일된 코딩 컨벤션으로 가독성이 높은 코드 만들기
  • 변화에 대응하기 쉬운 프로젝트로 구조 정리하기
  • 신규 인력이 투입되더라도 인수/인계를 손쉽게 할 수 있을만한 프로젝트 만들기

그리고 마지막으로, 우리가 개발한 코드도 레거시 코드가 될 수 있음을 인정하되 최대한의 노력을 기울여 프로젝트에 이상적인 구조로 설계하자는 것이다.

제약 사항

서비스를 개발하고 운영하다 보면 여러 이유로 인해 기술 부채가 쌓인다. 지금 당장은 해결하기 버거운 분량의 코드 개선을 미봉책으로 덮어두고, 언젠간 이 기술 부채를 갚으러 돌아오겠다며 남겨진 주석들이 시간이 지날수록 늘어난다. 개발의 우선순위가 새롭게 구현해야 하는 화면과 기능에 쏠려있는 경우나, 주요 고객의 요구사항과 피드백을 빠르게 대응해야 해서 내실을 다지다 보면 부채가 쌓인다. 슬프게도, 앞의 두 경우를 제외하고 기술 부채가 쌓이는 이유는 대부분 개발 인력이 충분하지 못해서 일 것이다. 게다가 개발 인력이나 서비스 요구사항의 변동은 우리가 컨트롤할 수 없는 외부의 요인이므로 기술 부채를 해소하기 위한 인력은 보수적으로 배치할 필요가 있다.

그리고 팀원 모두 프로젝트에 투입된 지 얼마 되지 않아 도메인 지식이 충분히 쌓이지 않았다는 게 가장 큰 제약사항이다. 따라서 서비스의 완성도를 높이기 위해 해야 할 중요한 일이 추가되었다. 바로 ‘프로젝트 이해도를 높이면서 수정에 대한 리스크를 예측하고 관리하기’다.

어떻게 달성할 것인가

필자와 팀은 ‘우리는 이런 기술 부채를 모조리 해결할 것이다!’라는 큰 포부를 마음속에 품은 한편, 시간과 비용의 제약이 있는 현실과도 타협해야했다. 그래서 적정 비용을 투입하지만 준수한 결과가 나오도록 우선순위와 일의 범위를 조율하는 시간도 가졌다. 지금부터는 이상적인 의견들과 현실적인 문제를 어떻게 이어붙였는지 살펴보자.

앞서, ‘우리의 코드도 레거시가 될 수 있다’는 전제 아래, ‘외부 요인에 의한 인력과 우선순위 문제'를 해결하기 위한 방안은 '리팩터링 사이클을 한 번이 아닌 여러 번으로 나누기'였다. 개발 기간이 길어질 수록 리팩터링 사이클을 여러 번 돌리기 어려우므로, 한 번의 사이클에 보다 전략적으로 리소스를 투입해야 했다. 그리고 각 사이클마다 다른 부분에 중점을 두고 리팩터링을 할 예정이다.

그럼 ‘팀원의 도메인 지식이 충분치 않은 문제’는 어떻게 해결해야 할까? 이 부분은 조금 더 기본적이고 정석과도 같은 방법을 택했다. 각 페이지별 통합 테스트를 작성하면서 기초적인 리팩터링을 조금씩 진행하는 것이다. 기존 코드에는 테스트 코드가 존재하지 않았기 때문에 테스트 코드 작성은 필수였고, 통합 테스트를 작성하면서 각자 프로젝트의 도메인과 코드 베이스에 익숙해지도록 유도했다.

지금까지 한 일

아직까지는 통합 테스트 코드 작성이 급선무다. 통합 테스트 코드 작성은 QA 부서에서 제공한 T/C를 기반으로 진행 중이며, 1차적으로 리팩터링의 안전망 역할을 해줄 것으로 기대하고 있다. 동일한 효과를 기대할 수 있을 다른 테스트 방법론인 유닛 테스트와 E2E 테스트도 고민했다. 모듈 테스트의 경우는, 코드 레벨에서의 이해도는 높힐 수 있겠지만 서비스의 스펙과 도메인 지식은 얻을 수 없을 것이라 판단했다. 그리고 E2E 테스트는 QA 부서에서 TestCafe를 이용해서 진행하고 있기 때문에, 이중으로 작성하는 수고를 들일 이유가 없어서 도입하지 않았다.

지금까지 작성한 통합 테스트의 수와 실행 시간

지금까지 작성한 통합 테스트의 수와 실행 시간

그리고, 필자의 팀 ESLint 규칙과 Prettier 규칙을 적용해서 1차적인 코딩 컨벤션의 틀을 잡았다. 커밋 컨벤션과 CI를 이용한 PR Builder 적용, 프로젝트의 빌드와 배포 프로세스 정리 등을 진행했다. 업무 프로세스도 지속적으로 정리하고 있다.

앞으로 할 일

아직 첫 번째 리팩터링에 진행할 리팩터링의 범위와 사이클을 확정하지 못했다. 가장 시급한 몇 가지 리팩터링(상태 관리 모듈의 물리적 분리 등)과 프레임워크에 강하게 결합된 비즈니스 로직의 계층 분리가 예약되어 있긴 하지만, 테스트 코드 작성이 마무리 되는 시점에 다시 한번 계획을 정리할 예정이다.

프로젝트의 설계 구조를 변경할 큰 리팩터링은 아직 청사진 정도만 준비되어 있다. 팀원 모두가 통합 테스트 코드를 작성하다 보니 테스트 코드의 가독성을 높히는 방법과 테스트에 용이한 코드를 작성하기 위해 고민이 많아졌다. 그러다 보니, 한 단계씩 진행하면서 선호하는 방향이 조금씩 바뀔 수 있다고 판단해서 미리 모든 걸 정해두지 않았다. 추후 프로젝트 구조 설계가 마무리되면 후속 포스트로 게시할 예정이다.

그리고 일련의 리팩터링 과정에서 한 단계가 끝날 때 마다 프런트엔드 성능을 측정하고, 성능 최적화까지 진행하는 게 최종 목표다.

마치면서

프로젝트를 인수 한 뒤 필자와 팀원은 프로젝트의 여러 개선 포인트를 찾아냈다. 가장 먼저 ESLint 등의 정적 분석 도구를 통해 코딩 컨벤션부터 맞춰나가기 시작했고, 지금은 부족한 테스트 코드와 서비스 이해도를 채우기 위해 통합 테스트를 작성하고 있다. 테스트를 작성하면서 코딩 컨벤션에 대한 의견과 서비스 설계에대한 청사진도 지속적으로 그려나가고 있다. 아직까지는 프런트엔드 프레임워크를 바꿀지, 설계 리팩터링만 할지 개발 방향조차 정해지지 않은 상태다.

그럼에도 불구하고 이 글을 작성하게 된 이유가 있다면, 이번 리팩터링 대장정을 시작하면서 좋은 아이디어가 대화와 회의로는 많이 오갔지만 휘발된다고 생각이 들어서였다. 지금과 비슷한 리팩터링 및 의사결정 과정을 이전에 속했던 프로젝트에서도 동일하게 겪었지만, 결국 지금까지 남은 것은 필자의 머릿속에 남은 기억 조각들뿐이었다. (어느 순간부터는 불분명할 수 있는 기억을 반복해서 반추하는 자신을 발견하기도 했다. 보다 명확한 자료로 남겨둔다면 많은 개발자와 필자 자신에게도 언젠가 도움이 되지 않을까 하는 생각도 들었다.)

따라서, 리팩터링을 진행하면서 마주치는 문제를 해결하는 과정이나 고민한 내용들을 글로 기록해서 공유할 예정이다.

팀원들과 의견을 나누고 작업 순서를 정할 때 나온 말이 있었는데, ‘우리는 아직 준비를 위한 준비를 했다.’는 말이다. 앞으로 갈 길이 멀지만, ‘시작이 반이다.’라는 말을 위안 삼아 FE개발랩이 치열하게 고민하고 성장하는 과정을 지켜봐 주길 바란다.