[회고] TDD를 해보았는데요

young_pallete·2023년 3월 14일
2

회고

목록 보기
4/5
post-thumbnail

🌈 시작하며

최근에 애플리케이션을 만들 일이 있었고, jest를 사용하여 단위/통합테스트를 했다.
사실 테스트 코드를 짜보지 않은 건 아니다. 하지만 약 3일간 온전히 TDD로 처음부터 모든 개발 환경을 세팅하고, 끝까지 TDD로 구현한 경험은 이번이 처음이었다.

이 글은, 그때의 TDD에 대한 회고와, 앞으로의 테스트에 대해 생각해보고자 남 몰래 남긴다. 😎

회고의 방식은... 음

  • 간단한 내 TDD 방식을 소개하고,
  • KPT의 방식으로 전개해야겠다.

🚦 본론

TDD, 그게 무엇일까


(사진 출처: 하나몬 - TDD란?)

애초부터 테스트 코드를 먼저 짜고, 이를 충족하며 개발해나가는 방식이다.
즉, 애초부터 주어진 시나리오 대로 필수적으로 행해져야 할 테스트들을 미리 코드로 작성한 다음, 이를 만족하는 기능을 구현하는 것이다.

결과적으로 작은 단위에서부터 시작한 테스트들은 점차 컴포넌트의 모든 로직들이 '정상적으로 돌아간다'는 근거가 되고, 결과적으로 코드의 유지보수로 인한 위험을 최소화할 수 있기에 리팩토링 등의 생산성 역시 올라가는 효과가 있다.

처음 시작하는 내게, 분명 TDD라는 개발 방법론은 꽤나 달콤한 유혹이었다.

TDD, 왜 시작했을까

Test Driven Development, 테스트 주도 개발 방법론을 택한 건 다음과 같은 이유에서였다.

음... 이 기능은 정말 비즈니스 로직이 깨지면 위험한데...

아직 실무 경험도 턱없이 부족하지만, 적어도 지난 개발자로서의 삶을 살며 느꼈던 건, 개발자는 비즈니스와 리소스를 끊임없이 생각해야 한다는 것이었다.

그렇기에 TDD를 택했다. 적어도 인증 절차에서 조금이라도 깨지면 애플리케이션이 보안상 취약점이 노출될 터이니, 애초부터 제대로 만들자는 것이 내 생각이었다.

리소스는 분명 많이 들겠지만, '수 십 개의 로직 점검에 대한 비용 역시 덜어지는 건 마찬가지지 또이또이하겠지.' 라는 생각으로 TDD를 시작했다.

라이브러리 선택

일단 먼저, 나는 단위 테스트로 시작해서, 통합 테스트를 통해 전체 애플리케이션의 동작을 테스트하는 절차를 거쳤다.

이를 수행하기 위해, jest라는 라이브러리를 썼다.
사실 playwright를 사용할까도 고민했다. 내게 상대적으로 익숙하고, 자신있는 테스트 도구 중 하나였기 때문이다. (써본 적이 없다면 나중에 한 번 써보기를 추천한다. codegen을 비롯한 브라우저별 테스트는 정말 신세계다.)

그렇지만 일단 컴포넌트 및 핸들러를 독립적으로 아주 작은 단위의 기능까지 테스트하기에는 E2E Testing에 강한 playwright보다는, 단위 테스트로 많이 사용하는 jest가 안성맞춤이라 생각했다.

세부 테스팅 방법

일단 이 방법은 나중에 후술하겠지만, 꽤나 실패했다고 생각하는 편이며, TDD에 대한 많은 생각을 하게 만들었다.

단위 테스트

나의 경우, TDD를 할 때 다음과 같이 단위테스트를 작성했다.

  • 컴포넌트: 가장 추상적인 UI만 담당하는 컴포넌트들부터 개발했다. 여기서 UI가 제대로 렌더링이 되는지, 인증 시 에러 메시지는 정확히 나오는지를 테스트했다.
  • 커스텀 훅: 결국 커스텀 훅의 핵심은 상태를 변경하는 비즈니스 로직이었다. 이 비즈니스 로직이 원하는대로 상태값으로 변하는지를 테스트했다.
  • 전역상태: contextAPI를 사용했다. 전역 상태 역시 독립적으로 관리한 후, 상태 변화가 제대로 동작하는지를 간단히 살펴봤다.
  • 유틸: 아무래도 내가 맡은 기능이 고객 입력에 있어 꽤나 중요하기에 validator 등에 대한 모든 엣지케이스들을 탐색했다.

통합 테스트

통합테스트의 경우에는, 가장 구체적인 컴포넌트로부터 API가 내려지면, 가장 추상적인 하위 컴포넌트들까지 렌더링이 제대로 되는지를 검증하는 절차로 이어졌다.

이를 진행하기 위해서는, API를 모킹해야했다.
왜? 기본적으로 테스팅 환경은 모든 의존성을 제거해야 한다.
이때, API의 응답환경은 서버의 '상태'에 결과값인 View가 의존성을 갖게 된다. 즉, 테스트 결과에 대해 원치 않는 Side Effect가 발생한다는 것이다.

그렇기에 msw라는, 서비스 워커를 통해 API 요청을 가로채서, 이에 대한 반환값을 mock화 시켜주는 라이브러리를 가져왔다. 이를 통해 API 요청에 대한 로딩/결과/에러 등이 컴포넌트에서 동작하는지 테스트했다.

결과

결과적으로 인증에 관한 애플리케이션 페이지 구축 작업은 3일만에 약 200개의 테스트 케이스 추가라는 지독한 결과를 만들었다. (3일은 정말 3시간만 잔 것 같다.)

그만큼 TDD에 대해 정말 알고 싶었고, 정말 많이 도전해봤고, 그렇기에 이제는 글로 말할 수 있었던 것 같다.

일단 여기까지가 내 TDD 방법에 대한 기본적인 context이다.
기본 환경을 보여주고 싶으나, 내게는 권한이 없다 😭

Keep - 잘한 점

상대방이 믿을 수 있는 코드를 짰다

테스트 코드들이 나의 모든 로직을 검증할 수 있다. 그렇기에 상대방에게도 안정성을 줄 수 있다고 생각한다.

특히 이러한 방법론은 실무의 PR에서 더 효과적이지 않을까 싶다.

코드는 팀원들이 다같이 책임져야 한다. 그렇기에 PR 문화가 존재한다.
하지만 때로는 이러한 '공동 책임'이라는 말로 인해 PR 문화가 움츠러들 때가 있다.
실제로 나 역시 실무에서 PR 문화를 만드려 했으나 꽤나 어려웠던 기억이 있다.
예컨대 다음과 같은 동료들의 질문에 부딪힌 경험이 있다.

  • 온전히 개발에 집중하고 싶은데, 시간이 너무 많이 걸려요.
  • 근데, 이거 결국에 누가 봐요? 결국에 이 문서 찾을 수 있을까요?
  • 만약 누군가가 이 문서를 못 찾는다면, 우리끼리 열심히 적어봤자 아닌가요?

TDD는 이러한 의문과 PR의 한계를 말끔히 해결해줄 수 있다.

  • 온전히 개발에 집중하고 싶은데, 시간이 너무 많이 걸려요.
    -> PR로 일일이 로직이 맞다는 것을 설명하지 않아도, 테스트 코드가 증명한다.
  • 근데, 이거 결국에 누가 봐요? 결국에 이 문서 찾을 수 있을까요?
    -> 해당 모듈이 위치한 곳에 테스트 파일만 놓으면 쉽게 로직의 유효성을 입증할 수 있다.
  • 만약 누군가가 이 문서를 못 찾는다면, 우리끼리 열심히 적어봤자 아닌가요?
    -> 지속적인 테스팅을 통해 미래의 동료나, 현재의 동료 모두에게 개발할 용기를 준다.

여러 명이 관리하기 위해서는, 각자에게 해당 코드의 동작에 대한 지속적인 감시를 요청하기보다는, 테스트 코드를 작성함으로써, 이 로직이 분명 성공한다는 믿음을 주는 것이 더욱 좋은 것 같다고 생각했다.

그러한 측면에서, 분명 내 테스트 코드는, 다른 사람들에게 이 코드가 잘못되지 않음을 증명할 수 있는 든든한 근거가 되었다고 생각했고, 이는 잘했다고 생각한다.

최소한의 QA로 개발에 집중할 수 있다

보통 로직을 고치거나 변경하면, 항상 Side Effect를 염두해야 한다.
그렇기에 나도 보통 개발을 할 때에는 코드를 치는 만큼이나 QA를 많이 하는 경향이 있다.

그런데 이번에 TDD를 하며 정말 신기한 경험을 했다.
평소에 로컬 서버를 켜고 실제 동작을 테스트하는 시간이 거의 없었던 것이다.

이미 이 기능에 대한 테스트 코드를 만족하면 기능이 된다는 것을 확신하기 때문에, 실제 동작을 개발 서버를 켜고 확인하지 않아도 됐다.

이러한 점은, 온전히 내게 개발할 시간을 부여하게 했다는 점에서 잘했다 생각한다.

테스트하며 얻은 좋은 코드 품질

정말 작은 단위부터 점차 올라가니, 오히려 구체적인 컴포넌트를 제작하는 데 더 수월했던 것 같다. 단순히 추가만 해도, 동작할 것 같은 확신이 든다고 해야 하나.

또한, 컴포넌트를 테스팅하는 데 있어 mock을 넣어주기 위해서는 props를 통해 넘겨 받는 방식으로 개발해야 했다. 이렇게 props로 넘겨받는 컴포넌트를 개발한 결과, UI 컴포넌트는 더욱 로직과 분리되었다.

결과적으로 TDD를 진행하면서 더욱 추상성 높은 컴포넌트 개발이 가능했고, 좋은 컴포넌트 개발이란 무엇인지 많이 생각하게 됐다.

Problem - 못한 점

테스트 코드 작성에 시간이 너무 많이 걸렸다

아무래도 이건 내가 초보인 것도 있었다.
jest를 거의 사용하지 않았다고 보아도 무방할 정도로 단위 테스트에 대한 지식이 없었다.
(그렇기에 커뮤니티의 힘을 받고자, 가장 많이 사용하는 jest를 택한 것도 있다)

그렇지만, 모든 단위 테스트들을 하나하나 써야 한다는 강박관념으로 인해, 테스트하는 시간이 실제 개발 시간의 2배 이상을 웃돌 정도로 힘들었던 것 같다. 나중에 모든 기능을 완료했을 때, 3일 동안 테스트 코드가 220개 돌아갈 정도로 작성했다는 건 충격적이었다.

어떻게 보면 정말 열정적이었지만, 아까도 말했듯이 개발자는 리소스를 고려해야 한다.
분명 내가 이 테스트 코드를 덜 작성했다면? 더 많은 기능을 최적화하고 추가하지 않았을까.

그런 것들 역시 정말 좋아하는 것들인데, 짧은 시간 내에 TDD를 하면서 꽤나 포기하고 타협했다는 점은 스스로도 좋은 개발자인지 의구심이 들기도 했고, 마음이 아팠다.

물론 이제는 홀로 피드백을 하며, 내가 테스트 코드하면서 부족했던 점들을 생각하고, 좀 더 잘 작성할 용기와 자신감을 얻었다. 😎 역시 결국 헤맨 만큼 자기 땅이 아닐까 싶다.

테스트 코드의 Nesting이 과했다

당시에는 정말 jest를 잘 써보질 못했으니, 다양한 코드들을 살펴봤다.
그중 beforeEach, afterAll과 같은 작업들로 이루어진 걸 많이 접했다.

당시에는 이게 맞다고 생각했다. 왜냐하면 다른 개발자들이 이렇게 많이 쓰기 때문이다.
그렇기에 내 Nesting은 정말 4~5 중첩까지 이어질 정도로 심해졌다.

중첩할 때에는 잘 몰랐는데, 나중에 나를 정말 곤혹스럽게 한 것은, 테스트 코드를 유지보수할 때였다.

곳곳에 도처한 beforeEach는 분리 및 리팩토링할 때마다 말썽이었고, 결과적으로 이를 유지보수하는 데 꽤나 애를 먹었던 기억이 난다.

Try - 좋은 테스트하기

이렇게 테스트하지 않을 것이다.

이후 스스로 현타(?)를 느끼면서 좋은 테스트 코드란 뭘까...하며 인터넷을 서칭했는데, 좋은 글이 보였다.

Kent - Avoid Nesting when you're Testing

이 글을 만약 시작할 때부터 보았다면 내 코드는 달라졌을까. 😭 싶었다.
따라서 앞으로는 다음과 같이 하기로 맹세했다.

  1. beforeEach, afterAll과 같은 공통의 작업을 처리하는 함수는 지양한다.
  2. 다양한 컨텍스트를 중첩함으로써 생겨난 콜백은 최소화한다.

이 두 가지는 가독성도 매우 좋지 않을 뿐더러 유지보수가 매우 힘들다. 그렇기에 하지 않을 것이다.

이렇게 테스트할 것이다

그리고 다음 2가지는 하려 노력할 것이다.

  1. 공통의 로직을 재사용가능한 함수로 빼낸다.
  2. 최대한 인라인으로 Given When Then을 작성한다.

이렇게 하면 분명 훨씬 가독성도 좋아지고, 각자의 컨텍스트가 독립되어 있어 테스팅이 더 용이한 것 같다는 확신이 들었다.

또한 마지막으로 한 가지 더 추가한다.

  1. 당장 프레임워크를 바꿀 일이 없다면 비즈니스 로직 테스팅 -> 컴포넌트 작업 -> UI 단위 테스팅의 순서로 진행한다.

난 정말 이번에 누구보다 열심히 TDD를 했다. 그리고 이 질문을 3일 동안 100번은 한 것 같다.

TDD가 과연 생산성에 도움을 줄까

누구보다 열심히 했기에 나는 이 질문에 대한 답을 찾았다고 생각한다.

"테스트 코드 짜기 나름이다"

내가 착각했던 것 - TDD는 모든 테스트를 각각 해야 한다

나는 TDD는 모든 경우에 대해 떠올리고, 이를 다 만족해야 한다고 생각했다.
그렇기에 정말 사소한 것들도 테스트 코드를 짰다. 예컨대, UI에서 클릭하면 함수가 호출되어야 한다던지, 라벨이 보여야 한다던지... 등등 말이다.

물론 일일이 작성한다는 것, 커버리지를 올린다는 것은 전체 품질에 있어 좋으면 좋지 결코 나쁘지 않다.
그런데 중요한 건, 이 테스트 코드 때문에 다른 기능을 더 추가할 수 있지 않을까라는 기회를 놓친다는 생각이 들었다.

실제로도 분명, 나는 요구사항에 있는 기능들을 구현했지만, 다른 최적화들을 진행하지 못했었다. 결국, 나는 안정된 앱을 만들기는 했지만, 알잘딱깔센하게 내 리소스를 최적화하여 사용하는 데는 실패한 셈이다.

테스트의 커버리지에 현혹되지 말자.
개발 리소스가 넉넉하다면 모르겠지만, 한정적인 자원에서 커버리지 100%는 독이다.
테스트 코드 역시 결국 리소스를 고려해야 하는 업무 중 하나라는 것을 깨달았다.

TDD는 정말 이 앱에서 필수적인 기능이 깨지지 않도록 보호한다는 느낌으로 할 것이다.

TDD... 해야 하나?

테스트 코드를 먼저 작성하는 방법은 우아했지만, 이런 생각들이 머리 속에 무수히 생겨났다.

TDD는 정말 '완벽한 방법론'일까?

음... 나는 아닌 것 같다.
취지는 이해한다. 결국 우리는 일정 요구사항을 충족하는 기능을 구현해야만 하고, 이를 계속해서 확인하는 작업은 비효율적이기에 이러한 방법이 유효하다는 것은 인정한다.

그렇지만 좀 엉뚱한 면이 있다.
TDD는 결국 테스트 코드를 먼저 짜고, 이를 검증하는 방식으로 전개한다. 그렇지만, 테스트 코드를 나중에 짜도 요구사항에 대한 충족 여부를 검토할 수 있지 않은가?

또한, 프론트엔드를 개발하다 보면, 하루하루 회의마다 컴포넌트 요구사항이 엎어지는 경우가 허다하다. 이런 상황에서 테스트 코드를 먼저 짤 수 있을까?

나아가 애자일하게 짧은 시간에 스프린트하는 IT 문화에서, 이 코드를 먼저 짜는 것이 개발자의 생산성에 피해를 주지는 않을까? 팀에게 생산성을 줄 수 있기 때문에 필요하다면, 이를 할 수 있는 여건이 보장되어 있을까?

각 기능들을 테스트 코드를 작성하며 검증하는 방식이 얼마나 효과적인 것일까? 과연 단기간에 다양한 기능을 구현하고, 추후 필요한 테스트만 빠르게 검증하는 개발보다 더 좋은 것일까?

이런 계속된 물음 속에서, 다음과 같은 생각을 하게 됐다.
테스트 코드를 믿는다. 하지만 나는 TDD를 믿지는 않는다.

내게는 테스트 코드를 짜는 시간만큼이나, 일단 구현사항을 빠르게 올리고, 이것이 믿을 수 있는지 검증할 수 있는 테스트 코드를 짜는 방식이 더 유효하다고 생각하기 때문이다.

내가 잘못된 건지 다양한 자료들을 찾아보아야 했다.
그리고 해당 글이 가장 나와 비슷하다는 생각이 들었다.

우리는 생산성을 위해 추상화를 위한 라이브러리를 쓰고 있고, 이를 '어느 정도' 믿어주는 게 필요하다. (내가 해당 UI 렌더링 테스트를 위한 리소스를 확보하기 전까지 말이다.) 중요한 것은, 이벤트들로 일어나는 비즈니스 로직에 관련한 함수들을 검증하는 것이다.

생각보다 테스트 코드는 비싸다는 것을, 나는 그 시간에 더 많은 것을 할 수 있음을 이번에 값지게 배웠다.

👋🏻 마치며

내가 작성했던 코드들을 중심으로 예시를 들고 싶지만, 안타깝게도 내게는 권한이 없기 때문에 그저 모양 없는 회고만 진행하게 됐다.

지금 2시간 내내 노트북으로 회고를 정신없이 썼음에도 불구하고, 피로감을 전혀 느끼지 않는 것을 보아 꽤나 나는 테스트와 이 글에 진심이었구나를 깨달았다.

여튼, TDD든 뭐가 중요하랴.
결국 나는 어떠한 방식으로든, 내가 작성한 코드가 최대한 효율적이고 안정적으로 돌아가도록 모든 방법을 다 할 것이다. 난 그런 개발자니까. 😎

참고자료

profile
People are scared of falling to the bottom but born from there. What they've lost is nth. 😉

0개의 댓글