Welcome To

Schnee’s Blog!

테스트 코드? 잘안짜요.

평화롭던 6월 초... 코드스쿼드 학원에는 전 기수 선배들이 찾아오시게 되었습니다.

선배분들의 취업 과정과 현업에서의 경험들을 이야기하던 중, 저는 질문을 하나 하게 되었습니다.

현업에서는 테스트 코드에 대해서 어떻게 생각하나요?

돌아오는 답변은 다음과 같았습니다.

현업에서는 테스트 코드를 잘안짜요. 안그래도 개발하기 급급한데 테스트까지 짜면.. 어휴 비용도 두배로 들고 기능이 바뀔 때 마다 테스트 코드도 깨지니 잘 안하는거 같아요~

이 말을 들은 저는 충격 😮 을 먹었습니다. 그 전까지 테스트 코드에 관심을 갖고 학습하고 있었는데, 필요하지 않은 것을 배우고 있었나? 라는 생각이 조금씩 들기 시작했습니다.

정말 다른 프론트엔드 개발자분들도 이렇게 생각할까? 라는 의문도 들어 저는 프론트엔드 테스트에 대한 블로그 포스트들을 찾아보기 시작하였습니다.

이번 포스트에서는 테스트 코드의 단점들을 해석하고, 테스트 코드들은 어떤 장점을 가지고 있는지, 그리고 저는 앞으로 테스트 코드에 대해서 어떤 방향성을 가지고 나아가야 할 지 고민한 점을 다루어 보도록 하겠습니다.

단점

테스트 코드를 작성하면서 개발자에게 생기는 불편함에는 무엇이 있을까요?

제가 알아본 부분들 중에서 개발자가 느끼는 가장 큰 단점들은 다음과 같습니다:

서비스의 기능이 변경되거나 확장될 때마다 테스트 코드도 함께 수정이 필요하다.

안그래도 프론트엔드에서는 UI가 사용자의 요구에 따라서 매일매일 바뀔 수 있는데, 컴포넌트를 수정할 때 마다 테스트 코드도 같이 변경되야 하는 상황이 벌어질 수 있습니다.

적용한 기술에 따라서도 테스트 코드가 전체적으로 변경되야 되는 수고로움이 있을 수 있습니다. 실제로 저는 이슈 트래커 미션 때 이런 상황을 겪어보았습니다.

다음은 새로운 라이브러리 적용으로 에러가 났던 테스트 코드입니다.

export default { title: "Login/Login", component: Login, } as Meta; const Template: StoryFn = () => <Login />;

위 로그인 컴포넌트에서 저는 컴포넌트 간의 경로를 설정하기 위해 react-router 를 사용하였고, 서버 상태를 관리하기 위해 react-query 를 사용하였습니다.

라이브러리를 적용한 후, 구현을 진행하면서 일주일을 마무리 할 때 쯤 다음 에러들이 나타났습니다.

Storybook 리액트 라우터 버그

Storybook 리액트 쿼리 버그

이는 useNavigateuseMutation 훅을 쓰지만, 로그인 컴포넌트를 라우터쿼리 클라이언트 컴포넌트로 감싸주지 않았기 때문에 생겼던 버그였습니다.

이를 다음과 같이 decorators 프로퍼티에 라우터를 감싸도록 설정하여 해결하였습니다.

export default { title: "Login/Login", component: Login, decorators: [ (Story: StoryFn) => ( <QueryClientProvider client={queryClient}> <MemoryRouter> <Story /> </MemoryRouter> </QueryClientProvider> ), ], } as Meta; const Template: StoryFn = () => <Login />;

위와 같은 변경은 한 테스트 코드에만 있는게 아니라 다수의 테스트 코드에 변경을 초라할 수 있기 때문에, 개발 비용이 더 많이 들어간다라고 많은 개발자들이 느끼는 것 같습니다.

러닝 커브가 높다.

입출력이 명확한 백엔드 테스트에 비해 프론트엔드에서는 한 컴포넌트 안에서도 비동기 처리, 이벤트 핸들링과 같은 사용자 상호작용에 의해 테스트할 케이스들이 복잡하고 양도 많습니다.

데이터를 비동기적으로 요청하는 테스트를 작성하려면, 여러 상태 (로딩, 성공, 실패) 를 고려해야 하고, 이 비동기적인 동작이 버튼 클릭에 의해 일어난다고 하면, 버튼이 클릭 되었을 때, 클릭 되지 않았을 때의 테스트 케이스도 작성해야 합니다.

테스트 케이스는 독립적이여야 하고, 그의 목적을 명확히 하며, 동일한 입력값에 동일한 결과값이 보장 되어야 합니다. 이를 위해 일반적으로 테스트 케이스를 given-when-then 구조로 작성합니다.

그리고 테스트 코드는 코드 내부동작의 순서를 검증하는 것이 아니라 사용자가 특정 동작을 수행했을 때 예상한 결과가 잘 나타나는지를 검증하는 것이기 때문에, 테스트 코드 안에 있는 동작들의 목적도 같이 생각해봐야 합니다.

테스트의 설명을 작성할 때도 신중해야 합니다. 테스트 설명을 명확히 작성하지 않는다면 오히려 테스트와 구현된 코드를 한줄씩 뜯어봐야 하는 상황이 생길 수도 있습니다. 반대로 설명을 명확하고 이해하기 쉽게 작성한다면, 설명 그 자체만으로 명세 역할을 수행할 수 있습니다.

좋은 테스트 코드를 작성하기 위해서는 경험이 필수적이지 않을 수 없습니다. 혼자만의 사이드 프로젝트만으로는 이 코드가 읽기 쉬운 코드인지 아는 것이 어려울 수 있기 때문입니다.

기존에 있던 코드들을 모두 리팩토링 해야 한다.

많은 프로젝트에서는 테스트 코드를 아예 작성하지 않거나, 비즈니스 로직을 먼저 구현하고 테스트 코드를 작성하는 경우가 많습니다.

혹은 이미 완성된 프로덕트에 테스트 코드를 붙히는 경우도 많죠.

이러한 경우에는 명세서를 먼저 확인하거나 기존 코드를 읽으면서 동작의 흐름을 따라가야 하는 수고가 있을 수 있습니다.

그리고 이런 레거시 코드들은 아마도 테스트를 고려하지 않고 짜여진 경우도 많을 겁니다.

테스트를 고려하지 않았다는 것은 기능들이 다른 기능들에 의존하면서 구현되어 단일 책임 원칙이 지켜지지 않은 상태라는 겁니다.

이러한 기능들을 테스트하려면 어떻게 해야할까요?

전체 코드를 분리하고 중복을 제거하고... 아마 리팩토링이 긴 여정이 될 것입니다.

그리고 리팩토링 하는 과정에서 또 다시 버그가 생기고, 버그는 버그를 낳고, 결국에는 아예 손도 대지 못하는 상황이 펼쳐질 수 있습니다.

이러한 부분들 때문에 많은 프론트엔드 개발자들이 테스트를 꺼려 하는 것일지도 모릅니다.

장점

사실 테스트 코드가 나쁘다고 말하는 개발자는 아마 없을 겁니다.

그저 테스트 코드에 집중하다가 전체적인 프로젝트에 큰 영향이 가는 것에 두려움이 크기 때문에 이를 꺼려하는 것일 겁니다.

도대체 테스트 코드를 작성하면 일정이 늦어짐에도 강조되는 이유는 무엇일까요?

문서화로 협업 효율이 증가한다

테스트 코드는 문서 역할을 할 수 있습니다. 테스트에는 사용자 상호작용의 시나리오들이 테스트 케이스로 이루어져 있고, 각 테스트 케이스마다 어떤 상호작용이 어떤 결과를 나타내는 지를 설명합니다.

테스트 코드는 이러한 설명을 포함하고 있기 때문에 다른 팀원이 내 코드를 보더라도 테스트 코드의 동작 흐름을 통해 기능을 더 잘 이해할 수 있습니다.

혹여나 기능을 확장하는 도중에 의도와 동작을 잘못 이해하고 코드를 잘못 수정하더라도 테스트 코드를 통해 손쉽게 잘못된 부분을 알 수 있습니다.

디버깅 시간을 단축한다

사실 우리는 테스트 코드를 작성하지 않아도 이미 버튼을 클릭하고, 화면을 보면서 컴포넌트가 어떻게 렌더링 되는지 확인하며 테스트를 이미 하고 있습니다.

하지만 UI 들이 많아지면 많아질 수록 모든 UI나 코드들을 직접 확인하기가 기하급수적으로 어려워집니다.

심지어 버그가 클라이언트의 문제가 아닐수도 있을 수 있습니다.

단위 테스트를 작성한다면 사용자와 상호작용 하고 검증하는 부분을 자동화 시켜주기 때문에 전체 어플리케이션을 손수 테스트 해볼 필요가 없어질 것입니다.

테스트 코드를 작성하며 더 나은 설계를 할 수 있다

개발을 하면서 설계보다 구현이 먼저 앞서는 개발자들이 많습니다. 저도 노력은 하지만 예외는 아닙니다.

이런 방식으로 개발을 진행하다 보면 어느샌가 어플리케이션의 구조가 잘못됐다라는 것을 인지할 때가 있습니다. 심하면 겉잡을 수도 없이 리팩토링도 진행을 못하는 상황이 벌어질 수 있습니다.

테스트 코드를 작성하며 구현을 하면 테스트를 가능하게 하기 위해 기능을 재사용 가능하고, 독립적이고, 순수하게 구현하도록 노력합니다.

이 과정에서 내가 지금 무엇을 해야하는지 명확히 정의하고 개발을 시작하게 됩니다. 또한 테스트 시나리오를 작성하면서 다양한 예외상황에 대해 생각해 볼 수 있으며 이는 완성도 높은 설계로 이어집니다.

설계가 완성도 높은 상태로 개발이 진행된다면, 크리티컬한 버그가 생길 확률이 극도로 낮아지고, 전체적인 소프트웨어의 전반적인 설계가 변경되는 일을 방지할 수 있습니다.

앞으로 내가 나아가야 할 방향성

저는 이번 포스트를 작성하면서 테스트 코드에 대한 양쪽의 의견들을 읽어보았습니다. 프론트엔드 테스트의 단점들에 공감을 많이 합니다.

테스트 코드는 쓰면 쓸수록 똑같은 의미없는 일을 반복하고 있는 것 같고, 때로는 머리가 아프기도 합니다.

그럼에도 불구하고 저는 테스트 코드는 필요하다라는 쪽으로 마음이 가는 것 같습니다.

물론 작은 프로젝트에는 굳이 필요 없겠지만, 프로젝트가 커지면 커질수록 그 중요성은 배로 늘어나는 것 같습니다.

기능 구현의 속도도 중요하지만 기능이 어떤 이유로 동작하지 않으면, 이는 헛수고가 되기 때문에 저는 지속적으로 안정적인 프로젝트를 만드는 것이 더 중요하다고 생각됩니다.

나쁜 테스트 코드도 프로젝트에 부정적인 영향을 끼칩니다. 저는 이러한 부분들을 인지하고 더 좋은 테스트 코드를 쓰도록 학습하려고 합니다.

참고자료