웹 개발은 기능을 구현하는데에서 끝나는 것이 아닙니다. 구현한 기능이 의도대로 잘 동작하는지 반드시 확인을 해야합니다. 프론트엔드 개발에서 테스트 혹은 테스트 코드가 중요한 이유는 아래와 같습니다.
특히 프론트엔드는 사용자와 직접 상호작용하는 부분이 많은 분야입니다. 구현한 기능이 제대로 작동하는지, UI와 문제없이 조화를 이루는지 (개발 의도대로 동작하는지) 반드시 확인해야합니다.
컴포넌트, 커스텀 훅, 유틸 함수 등등. 프로젝트 각지에 흩어진 코드들을 보고 이 기능이 어떤 순서로 어떻게 동작하는지 파악하기는 어려운 일입니다. 테스트 코드를 작성해두었다면, 기능이 어떻게 동작하는지 더 빠르게 파악할 수 있습니다. 이는 코드의 유지보수성을 적절한 수준으로 유지할 수 있는 좋은 수단입니다.
그렇다면 테스트는 언제 실시해야 할까요? 전통적인 개발론에서 테스트는 기능의 구현이 완료된 이후에 실시하는 절차로 규정되어 있습니다. 그런데 전통적 방법론의 테스트에는 다음과 같은 문제점이 존재합니다.
구현이 완료되었다는 것은, 이미 많은 코드가 작성되었다는 뜻입니다. 버그가 발생했을 때 긴 코드의 어디가 원인인지 파악하는데 시간과 노력이 더 소모될 수 밖에 없으며, 다른 기능과 연결되어있는 경우 최악의 상황에서는 이미 구현한 기능의 일부 혹은 전체를 제거해야하는 치명적인 사태가 벌어질 수도 있습니다.
이미지 출처 : https://www.boardinfinity.com/blog/a-quick-guide-to-arrays-in-python/
기존에 존재하는 전통적인 소프트웨어의 개발 방식은 '폭포수 모델(Waterfall Model)'이라고 불리웁니다. 개발은 순차적인 접근법으로, 각각의 단계를 순서대로 진행하게 됩니다.
프로젝트의 목적, 기능, 사용자 요구사항 등을 정의하고 문서화하는 단계입니다.
요구사항을 바탕으로 시스템의 구조와 구성요소, 인터페이스, 데이터 처리 방법 등을 설계합니다.
설계된 시스템을 실제 코드로 변환하는 과정으로, 프로그래밍 작업이 주를 이룹니다.
구현된 시스템이 요구사항을 만족하는지 테스트하고 검증하는 단계입니다. 이 때 문제점을 찾아 수정합니다.
시스템이 배포된 후에 발생하는 문제를 해결하고, 필요에 따라 시스템을 업데이트하거나 개선하는 단계입니다.
폭포수 모델은 목표 달성을 위한 전체적인 계획이 명확하며, 순차적인 단계로 구성되어 이해하기가 쉽습니다. 따라서 단계별 문서화도 용이하며 전체 프로젝트를 원활하게 관리할 수 있습니다.
그렇지만 폭포수 모델은 아래와 같은 문제점들이 존재합니다.
유연성 부족: 초기 단계에서 정의된 요구사항에 대한 변경이 어렵습니다.
피드백 및 반복의 부재: 최종 제품이 나올 때까지 사용자나 클라이언트의 피드백을 반영하기 어렵습니다.
위험 및 문제의 늦은 발견: 문제점이나 위험 요소가 프로젝트 후반에야 발견될 수 있어, 수정 비용이 커질 수 있습니다.
이미지 출처 : https://asana.com/ko/resources/agile-methodology
애자일 방법론은 이런 폭포수 모델의 단점을 해결하기 위해, 개발을 빠르고 유연하게 진행하는 것을 목표로 합니다. 지속적이고 신속한 개발 사이클을 반복함으로써 소프트웨어를 빠르게 개선할 수 있다는 장점을 지니고 있습니다.
프로젝트의 목표를 설정하고, 일정, 자원, 작업의 우선순위를 계획합니다. 이 단계에서는 프로젝트의 범위와 기대치를 명확히 정의합니다.
사용자의 필요와 프로젝트의 요구사항을 파악하고 문서화합니다. 애자일 방법론에서는 이러한 요구사항이 변할 수 있다고 보고, 유연하게 대응합니다.
시스템의 구조와 구성 요소를 설계합니다. 애자일 방법론에서는 설계가 반복적이며, 개발 과정에서 계속 수정될 수 있습니다.
실제 코드를 작성하고 기능을 개발합니다. 이 단계는 짧은 주기로 반복되며, 각 반복마다 작동하는 부분 제품을 생성합니다.
개발된 기능의 오류를 찾고 수정합니다. 애자일 방법론에서는 테스팅이 개발 과정과 밀접하게 연결되어 지속적으로 이루어집니다.
개발된 제품의 부분적인 또는 전체적인 기능을 검토하고, 이해관계자의 피드백을 받습니다.
개발된 소프트웨어를 사용자에게 제공합니다. 애자일에서는 자주 배포하고, 빠른 피드백을 통해 개선합니다.
위 단계들은 반복적으로 수행됩니다. 각 반복은 일반적으로 1-4주 정도의 짧은 사이클로 구성되며, 각 사이클의 끝에 사용 가능한 소프트웨어를 배포합니다.
애자일 방법론은 언제 제시될 지 모르는 클라이언트의 요구사항, 사용자의 피드백 등에 신속하게 대응할 수 있다는 장점이 존재하지만 그 한계가 뚜렷하여 일선 개발자들의 강한 비판을 받고 있는 이론이기도 합니다.
위와 같은 맹렬한 비판론도 존재한다.
간단하게 정리해보자면. 애자일 방법론의 적용을 위해서는 우선 다양한 요구사항을 명확하게 정리해서 개발팀에 전달해야하는 능력을 보유해야 하며, 개발팀은 시시각각 언제 쏟아질지 모르는 요구사항을 반영할 능력을 갖추고 있어야 합니다. 이런 것들이 제대로 갖추어져 있지 않다면 애자일 방법론을 사용한다는 것은 개발팀에 대한 심각한 부담으로 이어질 수 밖에 없습니다.
사실 애자일 방법론에 대한 비판은 '애자일'에 대한 개념을 제대로 이해하지 못한 상태에서 성급하게 이론을 도입함으로써 발생하는 갈등이 주된 원인입니다.
애자일이라는게 좋은 이론이라고 한다. 우리도 사용해보자.
그냥 자주 회의하고, 요구사항 반영해서 수정 많이하면 애자일 아닌가?
올바른 애자일 방법론에서, 제기되는 요구사항은 우선 분석을 거쳐 실제로 반영해도 되는 내용인지 파악해야합니다. 또한 이미 수정이 이루어졌거나, 수정 작업이 진행되고 있다면 중요도를 기준으로 작업을 중단해야하는지 혹은 다르게 진행해야하는지를 신중하게 결정해야합니다. 마지막으로 모든 작업은 보유한 자원 내에서 소화가 가능한 것들인지 따져봐야 합니다.
지속적인 통합 (Continuous Integration, CI): 개발자들이 자주 코드를 공유 저장소에 병합함으로써, 소프트웨어의 통합 문제를 줄입니다. CI는 버그를 빠르게 발견하고 수정하는 데 도움이 됩니다.
테스트 주도 개발 (Test-Driven Development, TDD): 테스트를 먼저 작성하고, 이를 통과하는 코드를 작성하는 방식으로, 코드의 품질을 향상시키고 유지보수를 용이하게 합니다.
리팩토링 (Refactoring): 기존의 코드 구조를 개선하는 작업으로, 소프트웨어의 내부 구조를 변경하지 않으면서 가독성과 유지보수성을 향상시킵니다.
페어 프로그래밍 (Pair Programming): 두 명의 개발자가 한 컴퓨터를 사용하여 함께 코드를 작성하는 방식으로, 코드 품질을 향상시키고 지식 공유를 촉진합니다.
스크럼 (Scrum): 스크럼은 애자일 개발의 하나로, 짧은 반복 주기(스프린트)로 작업을 분할하고, 정기적인 회의를 통해 진행 상황을 검토하고 계획을 조정합니다.
사용자 스토리 (User Stories): 기능 요구사항을 사용자의 관점에서 간단한 언어로 기술합니다. 이를 통해 개발자들이 최종 사용자의 필요에 중점을 둘 수 있습니다.
애자일 모델링 (Agile Modeling): 소프트웨어 모델링을 간소화하고 필요한 최소한으로 유지합니다.
칸반 (Kanban): 작업 항목을 시각화하여 작업의 흐름을 관리하고, 작업의 우선순위를 조정합니다.
애자일 방법론의 실천을 위해서는 우선 제대로 된 '조직'이 필요합니다. 프론트엔드 개발자를 준비하는 저의 입장에서는 애자일 방법론을 적용해서 프로젝트를 진행한다는 건 불가능한 의미이지만, 애자일 방법론이 무엇인지 이해하고 혼자의 힘으로 할 수 있는 수준까지는 애자일을 경험해보려 합니다.
현실적으로 따져봤을 때, 제가 할 수 있는 수준은 테스트 주도 개발(Test-Driven Development, TDD) 정도라고 판단하였습니다. 물론 TDD도 혼자 하는 규모로 제대로 할 수 있다고 말하기는 어렵지만 개인 학습의 차원을 벗어나지 않는 선에서 할 수 있는 만큼 경험해보려 합니다.
테스트 주도 개발(Test-Driven Development, TDD)은 소프트웨어 개발 방법론의 하나로, 테스트를 개발 과정의 중심에 두는 접근 방식입니다. TDD는 개발자가 실제 코드를 작성하기 전에 테스트 케이스를 먼저 작성하도록 합니다. 이 방식은 설계의 품질을 향상시키고, 버그를 줄이며, 코드의 유지보수를 용이하게 하는 것을 목표로 합니다.
이미지 출처 : https://zdodev.github.io/swift/tdd/Test-Driven-Development/
TDD는 아래의 간단한 순환 과정을 따릅니다:
테스트 작성: 개발할 기능에 대한 테스트 케이스를 먼저 작성합니다. 이 시점에서 테스트는 실패합니다(이를 'Red' 상태라고 함).
최소한의 코드 작성: 테스트를 통과할 수 있을 만큼의 최소한의 코드만 작성합니다('Green' 상태).
리팩토링: 작성된 코드를 정리하고 최적화합니다. 이 단계에서는 코드의 효율성, 가독성, 구조 등을 개선합니다.
이 과정을 반복함으로써, 소프트웨어는 점진적으로 구축되고 개선됩니다.
테스트 케이스 정의: 개발하려는 기능의 명세를 바탕으로, 해당 기능이 올바르게 동작하는지 확인할 수 있는 테스트 케이스를 정의합니다.
테스트 실행 및 실패 확인: 아직 기능이 구현되지 않았으므로, 테스트는 실패해야 합니다. 이는 TDD의 중요한 단계로, 테스트가 올바르게 작성되었는지 확인합니다.
기능 구현: 테스트를 통과할 수 있는 최소한의 코드를 작성합니다. 이때, 코드는 간결하고 명확해야 합니다.
테스트 통과 확인: 작성된 코드가 테스트를 통과하는지 확인합니다. 테스트를 통과하면 기능이 올바르게 구현된 것입니다.
리팩토링: 코드를 리팩토링하여 개선합니다. 이 과정에서 코드의 품질을 높이며, 중복을 제거하고, 가독성을 향상시킵니다.
반복: 다음 기능 또는 테스트 케이스로 넘어가서 위의 과정을 반복합니다.
향상된 코드 품질: 테스트를 먼저 작성함으로써, 개발자는 더 깔끔하고 오류가 적은 코드를 작성하게 됩니다.
버그 감소: 코드를 작성하기 전에 테스트를 수행함으로써, 초기 단계에서 버그를 발견하고 수정할 수 있습니다.
명확한 요구사항 이해: 테스트를 먼저 작성하면, 개발자는 요구사항을 더 명확하게 이해하고, 이에 맞춰 개발할 수 있습니다.
리팩토링 용이성: 안정적인 테스트 스위트가 있으면, 코드를 리팩토링하거나 변경하는 것이 더욱 용이합니다.
문서화의 대체: 테스트 케이스들은 어떻게 코드가 작동해야 하는지에 대한 일종의 문서 역할을 합니다.
개발자 자신감 향상: 테스트를 통과하는 코드를 작성함으로써, 개발자는 자신의 코드에 대해 더 자신감을 가질 수 있습니다.
학습 곡선: TDD를 효과적으로 사용하기 위해서는 테스트 작성에 대한 충분한 경험이 필요합니다. 즉, 새로운 개발자들에게는 TDD의 학습 곡선이 가파를 수 있습니다.
초기 개발 시간 증가: 테스트를 먼저 작성하므로, 단기적으로는 개발 시간이 늘어날 수 있습니다.
유지 관리 필요: 테스트 케이스 역시 코드의 일부분이므로, 소프트웨어가 진화함에 따라 테스트 케이스의 유지 관리가 필요합니다.
복잡한 시나리오에 대한 어려움: 일부 복잡한 시나리오나 UI 테스트 같은 경우 TDD를 적용하기 어려울 수 있습니다.
과도한 설계: 때때로 개발자는 테스트를 통과하기 위해 필요 이상으로 복잡한 설계를 할 수 있습니다.
테스트는 품질을 유지하고, 사용자 경험을 보장하며, 개발 과정의 효율성을 높이는 데 결정적인 역할을 합니다. 폭포수 모델과 에자일 방법론은 각각의 장단점을 가지며, 프로젝트의 특성에 맞게 선택되어야 합니다. 특히, TDD 같은 실천법은 에자일 방법론을 구현하는 데 있어 중요한 역할을 하며, 개발 과정을 지속적으로 개선하는 데 도움을 줍니다.
소프트웨어 개발의 세계는 끊임없이 변화하고 있으며, 이러한 변화에 효과적으로 대응하기 위해서는 적절한 방법론과 실천법의 선택이 중요합니다. 결국, 이 모든 방법론과 실천법은 하나의 공통된 목표를 향해 나아가고 있습니다:
더 나은 소프트웨어를, 더 빠르고 효율적으로 개발하며, 최종 사용자에게 최상의 경험을 제공하는 것입니다.
개발자로서 이러한 지식을 갖추는 것은 단순히 기술적인 능력을 넘어서, 더 나은 소프트웨어 솔루션을 창출하고, 끊임없이 변화하는 기술 환경에서 발전을 지속하는 데 필수적입니다.
다음 글에서는 React.js에서 이런 TDD 개념을 어떻게 적용하고, 사용할 수 있는지 정리해보겠습니다.