티스토리 뷰
배경
'프론트엔드에서 테스트 코드가 중요하다 vs 중요하지 않다' 항상 나오는 말이다. 시리즈 b 스타트업에서 일하는 3년차 프론트개발자인 나의 시선은 어떨까? 정리되어 있지 않은 내면의 생각들을 정리해보고 싶어졌다. 아마 조금만 시간이 지나면 지금 나의 생각도 바뀌게 될 것이다. 생각이 바뀌기전에, 지금의 기억을 잊어버리기 전에 정리해보겠다.
테스트 코드와 개발 시간
최근에 유스콘이라는 곳에서 `자동차 정비사 개발자가 되다`라는 세션을 들었다. 정비를 하려면 자동차를 뜯고 고장난 곳을 고치고 다시 조립해서 테스트를 한다고 했다. 그리고 정비가 잘못되었다면 다시 뜯어야하는데 길게 걸리는 경우는 일주일동안 뜯어야하고 힘도 많이 든다고 했다. 그런데 개발자는 딸깍 하니 테스트 코드가 바로 나오니 얼마나 쉽냐는 얘기를 했다. 참 맞는 말이다. 물론 개발자에게 테스트 코드를 넉넉하게 작성할 시간을 주는 경우는 없지만 정비와 비교하면 말도 안되게 편하다. 테스트 환경 세팅, 모킹 등 해야할것도 많고 코드의 수명이 얼마나 갈지도 모르지만 테스트 코드에 대해 많이 긍정적으로 생각하게 되었다.
그렇다고 해도 스타트업에 다니는 프론트 개발자가 모든 곳에 빡빡하게 테스트 코드를 넣을 수는 없는 노릇이다. 결국은 테스트 코드 비중을 높이면 높일수록 개발 시간은 길어질 수 밖에 없다. 하지만 버그가 발생해서 추가 개발하고 hotfix가 나가고 전체를 디버깅하게되면 테스트 코드를 작성한 경우보다 더 많은 시간이 소요될 수도있다.
유의미한 테스트 코드
테스트 코드와 개발 시간 두마리 토끼를 모두 잡을 수는 없을까? 개인적인 경험에서 유의미하다고 생각한 테스트 코드를 적어보겠다.
테트리스
나는 사이드 프로젝트로 테트리스를 구현했다. 테트리스는 일반적인 웹을 구현할 때와는 상황이 조금이 다르다. api 통신이 없고 복잡한 로직들이 많다. 이런 경우 테스트 코드가 굉장한 힘을 발휘하게 된다. 일반적으로 프론트엔드에서 테스트 코드가 호불호가 갈리는 원인을 생각해보면 너무 많은 모킹이고 그중에서도 api 통신이다. 그런데 테트리스는 api 통신이 없고 로직도 복잡하다. 테스트 코드를 사용하기 너무나도 좋은 환경이다.
처음부터 테스트코드를 작성하다보니 기본적인 테트리스만 구현했는데 100개가 넘는 단위테스트가 만들어졌다. 처음부터 테스트를 짜기 쉬운 구조로 만들다보니 생각보다 시간 소요가 많이 되지 않았다. 오히려 전체 구조를 변경할 때 안정적으로 빠르게 마이그레이션할 수 있었다.
코드스피츠에서 테트리스 강의를 들었을 때 도메인과 네이티브를 항상 분리해야된다는 얘기를 들었다. 이걸 리액트에 적용하자면 유틸함수, 모듈 등으로 독립적인 코드를 만들고 해당 코드와 리액트 코드를 결합시켜서(useState, hook, contextapi 등등) 브릿지를 만들고 컴포넌트에 연결을 하는 형태다. 또한 컴포넌트에서도 props를 이용해 계층분리를 할 수 있다. 이렇게 만들면 테스트를 짜기 쉬운 구조가 만들어진다.
사실 이런 요구사항은 프론트엔드보다는 백엔드에서 주로 처리한다. 그러면 백엔드 관점에서 바라보자. 백엔드에서 테트리스를 구현하는데 각각의 게임들이 db에 쌓인다고 가정해보자. 즉 비지니스 로직과 db가 만나면서 서비스 계층 테스트가 어려워진다. 백엔드에서 테스트코드를 쓸 때 가장 큰 병목지점은 db다.(개인적인 경험) db를 다 모킹하고 테스트하다보면 결국 프론트엔드에서 api 모킹해서 테스트하는 것과 다를바가 없어진다. 따지고보면 프론트든 백엔드든 테스트케이스가 어려워지는 경우는 모킹이다. 그러기 위해서는 모킹없는 테스트를 작성하는게 기본이 되어야한다. (물론 모킹이 필요한 경우도 있다.) 백엔드에서 좋은 테스트코드를 작성하는 방법에 대해 조금 공부했었는데 jojoldu님의 기술블로그글 이 많은 도움이 되었다. 해당 글의 핵심은 `외부 의존성이 필요한 통합 테스트의 범위를 좁혀야 한다.` 이다. 이건 프론트든 백엔드드 마찬가지다. 외부 의존성이 강한 코드는 테스트를 줄이고 깨끗한 영역을 만들어서 해당 영역을 위주로 테스트하면 된다.
백엔드에서 테트리스를 만들때도 마찬가지다. 도메인과 네이티브를 분리시키는게 곧 깨끗한 영역을 만든다고 볼 수 있고 이부분만 테스트하면 된다. 테트리스 같은 순수한 비지니스 로직은 백엔드, 프론트, 언어에 종속되지 않게 만들 수 있고 이건 테스트하기 쉬운 코드이고 동시에 좋은 코드다.
테트리스 링크
https://github.com/yoonminsang/TIL/tree/main/tetris
selector(select)
redux, react query, zustand 등의 라이브러리에서는 모두 selector 패턴을 이용해서 데이터를 가공하고 최적화한다. 그러다보면 selector에서 복잡한 요구사항을 처리할 때가 있다. 그중에서 나는 기간에 대한 selector를 만든적이 있다. 생각보다 기간에 대한 타입이 여러가지가 있었고 코드를 적어도 헷갈리는 경우가 많았다. 이 때 먼저 description을 생각하고 하나하나 테스트 코드를 작성하니 실수도 쉽게 잡으면서 작업을 완료할 수 있었다.
팁: selector에 들어가는 데이터가 너무 방대하다면 필요한 데이터만 뽑아서 테스트하면 된다. ex)foo,foo2,foo3,bar,bar2,bar3 데이터가 있을 때 foo,bar만 사용한다면 해당 데이터만 넣어서 테스트
짧은 시간투자로 테스트 코드 작성을 완료했다. 얻은 결과 값을 생각해보면 다음과 같다.
1. 프로젝트 전역에 사용되는 복잡하게 가공한 데이터의 안전성에 대한 확신이 생겼다.
2. 누군가 헷갈리는 정책을 물어봤을 때 테스트 코드를 보고 확실하게 얘기할 수 있다.
3. 요구사항이 변경되었을 때 기존 컨텍스트를 한눈에 파악하고 변경된 요구사항과 기존 요구사항을 비교하면서 쉽게 대응이 가능하다.
폼 테스트
나는 다음과 같은 복잡한 폼을 개발하게 되었다.
- 여러가지 권한에 따른 렌더링 분기처리
- 특정 필드를 변경할 때 모달 띄우고 동작을 결정하기
- 하나의 값을 변경할 때 다른 값 변경시키기
- 특정 필드를 변경할 때 토스트 띄우기
- input value에 따른 여러가지 message 처리
- DatePicker의 최소 최대가 여러가지 값에 영향을 받게 만들기
- 폼 제출시에 예외케이스 대응
- 폼 제출시에 특정 데이터에 따라 제출 데이터 변경시키기
react-hook-form을 사용하고 해당 훅에서 지원하는 contextapi를 이용해서 컴포넌트를 나누고 defaultvalue, onSubmit, 권한등은 props로 계층을 분리하는 등의 노력을 했지만 계속해서 복잡해지는 폼을 대응하기는 역부족이였다. 이때 우아한형제들 기술블로그에서 본 글이 생각났다. 단위 테스트로 복잡한 도메인의 프론트 프로젝트 정복하기(feat. Jest) 라는 글인데 내 상황 같았다. 글만 봤을때는 내 요구상황이 몇 배는 복잡해보였다. 그래서 테스트 코드를 도입했다. 사실 나는 공통 컴포넌트를 제외하고는 컴포넌트 테스트를 잘 하지 않는다. 그 이유는 output대비 공수가 너무 크기 때문이다. 그럼에도 불구하고 테스트 코드를 도입한 이유는 다음과 같다.
1. 비지니스로직이 복잡해서 유지보수가 쉽지 않다.
- 권한에 따른 분기가 많다.
- 기간에 따라 상호작용하는게 많다
2. 컨텍스트를 기억하는게 쉽지 않다.
- 1번 이유로 기존 컨텍스트를 기억하는게 쉽지 않다. 스쿼드내에서도 이게맞나? 저게맞나? 하면서 실제 화면으로 테스트해보고 코드를 보고 유추하고 이게 정책인지 버그인지 헷갈리는 경우가 많다.
3. 지속적으로 유지보수된다.
- 복잡하다고해서 유지보수되는 기능이 아닌데 무조건적으로 테스트코드를 도입하지는 않는다. 그 다음 개발도 이미 예정되어 있다.
- 해당 폼은 도메인 특성상 필수적이고 중요한 요소다.
작업을 간단하게만 정리해보면 다음과 같다.(테스트 방법론에 대한 글은 아니라서 구체적인 방법은 적지 않겠다. )
- 각각의 독립된 컴포넌트에 대해 단위테스트를 진행한다.
- 만약 컴포넌트 분리가 제대로 되어있지 않다면 먼저 컴포넌트를 통합하고 분리하는 작업을 진행한다. 컴포넌트 변경이 어렵거나 위험해보인다면 위험한 부분에 대해 가벼운 단위 테스트를 작성해도 좋다.
- 독립된 컴포넌트에서 해당 데이터와 관계없는 폼 데이터를 변경한다면 상위 컴포넌트에서 props로 관리하게 변경한다.
- contextapi(전역상태관리 라이브러리, react hook form 포함)를 이용해서 변경하는 것도 잘못된 방법은 아니지만 관련된 로직이 아니라면 외부로 의존성을 빼는게 유지보수 관점에서 좋다고 생각한다. ex) fooStartDate를 변경하는데 barStartDate가 변경되는 경우
- 상위 컴포넌트에서 통합 테스트를 진행한다.
- 하위 컴포넌트들은 이미 기본적인 단위 테스트가 되어있기 때문에 해당 컴포넌트에서 다루는 비지니스 로직만 테스트하면 된다. 또한 하위 컴포넌트에 props를 넘겨서 데이터를 변경하는 부분 정도만 테스트하자.
- 경우에 따라 데이터를 훅으로 분리하고 데이터만 테스트하는것도 방법이다. 요구사항에 따라서 테스트 구현체는 바뀔 수 있다. 중요한건 세부적인 모든 요소를 테스트하는게 아니라 놓쳐지기 쉽고 코어적인 부분을 테스트하는 것이다. 물론 렌더링테스트라면 세부적인 요소에 대해 테스트할수도 있다.
결과는 어떨까?? 일단 지금은 매우 만족스럽다. 자잘한 버그도 잡아냈고 웹, 앱이 다른 부분도 발견했고 ux 개선포인트도 잡을 수 있었다. 또한 앞으로 유지보수되는 부분에 대해서도 걱정없이 작업할 수 있게 되었다.
그럼 이제 생산성과 관련된 얘기를 해보겠다.
- 바쁜 일정속에서는 이 작업을 할 수 없다.
- 아무리 바빠도 한번쯤은 여유가 생긴다. 너무 조급해하지말고 여유를 가지고 작업하면 충분히 할 수 있는 일이다. 점진적으로 조금씩 도입하는 것도 좋은 방법이다.
- 테스트코드를 위해서 리팩터링을 해야한다는게 너무 비효율적이다.
- 사실 내가 작업할때는 거의 리팩터링을 하지 않았다. 기본적인 계층분리만 해놔도 대부분의 경우 추가 리팩터링 작업에 시간이 소요되지 않는다. 테스트 코드를 위해서 리팩터링하는게 아니라 유지보수가 어렵게 된 코드를 리팩터링한다고 생각하자.
- 테스트코드를 위한 세팅이 안되어있어서 초기 세팅이 어렵다.
- 초기 세팅은 한번만 해놓으면 쉽게 할 수 있다.
- 모킹해야되는 데이터가 너무 많다.
- 만약 단순히 모킹만 하고 실제 테스트에는 아무런 영향을 주지 않는 케이스라면 그냥 작성하면 된다. 어쩔 수 없다. 모킹때문에 복잡해지는게아니라 그냥 모킹만 하고 끝이라면 단순하다. 이건 테스트 코드에 익숙해지면 쉽게 처리할 수 있다.
- 실제 테스트코드에서 모킹된 데이터를 사용해야한다면 외부로 의존성을 뺄 수 있는지 생각해보자. 빼지 않는게 합리적이라고 생각한다면 기꺼이 모킹을 하자. 잘 작성된 코드라면 모킹해야되는 데이터가 맞지 않다고 생각한다. (msw로 서버 모킹해서 통합 테스트를 하는 경우는 지금 경우에 해당되지 않는다.)
한 줄 요약해보면 복잡한 폼에서의 테스트 코드 작성은 초기에는 시간이 들지만, 장기적으로는 안정성과 생산성을 높일 수 있다.
테스트코드를 몇 번 짜봤다고 복잡한 통합 테스트가 쉽게 짜지지는 않는다. 최소한 필요하다면 언제든지 테스트 코드를 작성할 수 있는 능력은 갖춰놔야한다고 생각한다.
storybook
스토리북은 이제 너무나도 당연한 툴이 되었다. 그래서 당연한 스토리북의 장점이나 특징은 따로 적지 않겠다. 내가 지금 생각나는 스토리북을 잘 활용한 경우는 두 경우가 있다.
1. 복잡한 카드 컴포넌트
특정한 api에는 type1(3종류)과 type2(4종류)가 존재한다. 또한 type1,2이외에도 여러가지 데이터들이 있고 데이터에 따라 조금씩 다르게 보여져야한다. 이걸 카드 컴포넌트로 렌더링해야한다. 어렵고 복잡한 요구사항이다. 지금은 storyook 얘기를 하고 있으니 UI 컴포넌트 관점에서만 바라보자.
카드 컴포넌트에 해당되는 인터페이스를 먼저 구현하고 세부적인 케이스들을 각각의 스토리로 만들자. (이때 story name을 상세히 적어두면 유지보수하는데 큰 도움이 된다. 10개가 넘어가는 복잡한 케이스라면 꼭 name을 자세히 적어두자.) 그리고 props에 필요한 데이터들을 각각의 스토리에 넣고 하나씩 UI 테스트를 진행하면 된다.
개발할때는 몰랐는데 지나고보니 테스트 방법론 중 BDD라는 패턴에 해당하는 것 같다. 즉 CDD, BDD 방법론을 이용해서 복잡한 컴포넌트를 구현하면 백엔드 인터페이스와 무관하게 복잡한 UI 케이스까지 테스트가 가능하다.
일정 복잡도를 넘어가게 된다면 스토리북을 작성하는게 훨씬 시간이 절약된다. 또한 다양한 케이스들을 직접 눈으로 보고 데이터까지 파악할 수 있기 때문에 유지보수가 쉬워진다.
2. 다양한 에러 케이스
다양한 에러 케이스를 모달로 보여줘야하는 요구사항을 받았다. 이때 에러가 발생하는 api는 4개고 제목을 제외한 모든 모달의 내용은 동일하다. 모달의 내용에는 4개의 에러에 따른 분기가 필요하다. 이걸 실제로 테스트하기는 매우 어렵다. 에러케이스를 세팅하고 4*4의 케이스를 테스트해야한다. 물론 실제 테스트는 필요하다. 하지만 그걸 직접 수동으로 처음부터 끝까지 테스트하는게 맞는걸까? 이때 코어 컴포넌트를 하나 만들고 컴포넌트를 확장해서 만들고 storybook을 이용하면 너무나도 쉽게 4*4의 케이스를 테스트할 수 있다. 나중에 누군가가 에러케이스가 궁금해요. 어떻게 뜨나요? 라고 물어본다면 스토리북을 켜서 보여주면 된다.
결론
짧은 경험에서 나온 글이지만 아니지만 누군가는 보고 인사이트를 얻었으면 좋겠다. 다음에 테스트에 대한 글을 쓸때는 지금 이 글이 부끄러워질정도로 성장했으면 좋겠다.
'기술' 카테고리의 다른 글
컴포넌트를 잘 만드는 방법 2편(리액트) (2) | 2024.02.13 |
---|---|
라이브러리없이 리액트 만들기(클래스형 컴포넌트) (0) | 2022.08.14 |
컴포넌트를 잘 만드는 방법(리액트) (7) | 2022.07.16 |