컴포넌트 동작 테스트, 내부 구현 방식이 아닌
Takashi Yamamoto
Infrastructure Engineer · Leapcell

소개
빠르게 변화하는 프론트엔드 개발 세계에서 강력하고 유지보수 가능한 애플리케이션을 만드는 것이 무엇보다 중요합니다. 컴포넌트가 복잡해짐에 따라 올바른 기능을 보장하는 것도 더욱 어려워집니다. 전통적인 테스트 접근 방식은 컴포넌트가 목표를 달성하는 방식, 즉 구현 세부 사항을 주장하는 함정에 빠지기 쉽지만, 관찰 가능한 동작, 즉 컴포넌트가 수행하는 작업은 그렇지 않습니다. 이는 리팩토링할 때마다 깨지는 불안정한 테스트로 이어져 개발 속도를 늦추고 코드베이스에 대한 자신감을 떨어뜨릴 수 있습니다. 이 글에서는 내부 구현 테스트와 외부 동작 테스트의 중요한 차이점을 살펴보고, 후자가 프론트엔드 컴포넌트에 더 효과적이고 지속 가능한 전략인 이유와 이 원칙을 실제적으로 적용하는 방법을 강조합니다.
핵심 개념 및 원칙
'방법'에 대해 자세히 알아보기 전에 논의를 안내할 몇 가지 핵심 용어를 명확히 해보겠습니다.
- 컴포넌트 동작: 특정 입력이나 이벤트에 대한 응답으로 컴포넌트의 외부에서 관찰 가능한 동작, 출력 또는 상태 변경을 의미합니다. 사용자 관점에서 컴포넌트가 '무엇'을 하는지에 해당합니다. 예를 들어, 버튼을 클릭하면 다이얼로그가 열립니다.
- 컴포넌트 내부 구현 세부 사항: 컴포넌트가 동작을 달성하기 위해 사용하는 비공Method, 상태 변수, 데이터 구조 또는 특정 렌더링 선택을 의미합니다. 컴포넌트가 내부적으로 '어떻게' 작동하는지에 해당합니다. 예를 들어,
isOpen부울 상태를 토글하여 다이얼로그를 표시하는지, 아니면 다이얼로그 컴포넌트 자체를 조건부로 렌더링하는지에 따라 다릅니다. - 블랙박스 테스팅: 컴포넌트를 블랙박스로 취급하는 것은 내부 작동 방식에 대한 지식 없이 입력과 출력에만 관심을 두는 것을 의미합니다. 이는 동작 테스트와 완벽하게 일치합니다.
- 화이트박스 테스팅: 컴포넌트의 내부 논리와 구조에 대한 지식을 갖고 테스트하는 것을 포함합니다. 유틸리티 함수 또는 복잡한 알고리즘에는 때때로 필요하지만, 구현에 대한 결합 때문에 UI 컴포넌트에는 일반적으로 권장되지 않습니다.
여기서 핵심 원칙은 캡슐화입니다. 잘 설계된 컴포넌트는 내부 구현을 캡슐화하고 상호 작용을 위해 공개 API만 노출합니다. 우리의 테스트는 이러한 캡슐화를 존중하여 사용자가 직접 작동하거나 부모 컴포넌트가 작동하는 것처럼 공개 인터페이스를 통해 컴포넌트와 상호 작용해야 합니다. 이렇게 하면 컴포넌트의 공개 동작이 일관성을 유지하는 한 내부 리팩토링에 대해 테스트가 탄력적으로 유지됩니다.
동작 기반 테스팅 채택
구현 세부 사항보다는 컴포넌트 동작을 테스트하는 철학은 효과적인 프론트엔드 테스팅의 초석입니다. 내부 코드가 리팩토링되어도 외부 동작이 변경되지 않는 한 테스트가 깨질 가능성이 적기 때문에 강력함을 촉진합니다. 테스트가 더 명확해져 사용자 여정에 집중하고 테스트 실패 이유를 이해하는 데 필요한 인지 부하를 줄이므로 유지보수성을 향상시킵니다. 궁극적으로 기술적 세부 사항에 관계없이 사용자 대면 기능이 예상대로 작동하도록 보장하여 애플리케이션에 대한 자신감을 키웁니다.
동작 테스트 방법
동작을 효과적으로 테스트하려면 다음 사항에 중점을 두어야 합니다.
- 컴포넌트 렌더링: 제어된 테스트 환경에 컴포넌트를 배치합니다.
- 사용자 상호 작용 시뮬레이션: 테스트 유틸리티를 사용하여 이벤트(클릭, 입력 변경 등)를 트리거합니다.
- 관찰 가능한 결과 주장: DOM 변경, 새 요소 표시, 텍스트 내용 업데이트 또는 모의 객체에서의 함수 호출을 확인합니다.
React와 React Testing Library(본질적으로 이 테스트 철학을 촉진함)를 사용하여 실제 예시를 통해 이를 설명해 보겠습니다.
간단한 Counter 컴포넌트를 고려해 봅시다.
// Counter.jsx import React, { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const increment = () => { setCount(prevCount => prevCount + 1); }; const decrement = () => { setCount(prevCount => prevCount - 1); }; return ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); } export default Counter;
동작 테스트(좋은 예):
// Counter.test.jsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import '@testing-library/jest-dom'; import Counter from './Counter'; test('renders initial count and updates on button clicks', () => { render(<Counter />); // 초기 상태 주장(동작) expect(screen.getByText('Count: 0')).toBeInTheDocument(); // 사용자 상호 작용 시뮬레이션(동작) fireEvent.click(screen.getByRole('button', { name: /increment/i })); // 업데이트된 상태 주장(동작) expect(screen.getByText('Count: 1')).toBeInTheDocument(); // 다른 사용자 상호 작용 시뮬레이션 fireEvent.click(screen.getByRole('button', { name: /decrement/i })); // 업데이트된 상태 주장 expect(screen.getByText('Count: 0')).toBeInTheDocument(); });
이 예시에서 우리는:
Counter컴포넌트를 렌더링합니다.- 표시되는 "Count: 0" 텍스트가 있는지 주장합니다. 이는 관찰 가능한 동작입니다.
- "Increment" 버튼에 대한
click이벤트를 시뮬레이션하여 사용자 상호 작용을 모방합니다. - 표시되는 텍스트가 "Count: 1"로 변경되었는지 주장합니다. 이는 또 다른 관찰 가능한 동작입니다.
이 테스트는 count가 내부적으로 어떻게 관리되는지(예: useState 사용, 리듀서 사용 또는 전역 저장소 사용) 신경 쓰지 않습니다. 특정 버튼을 클릭했을 때 표시되는 개수가 적절하게 변경되는지에 대해서만 신경 씁니다.
구현 세부 사항 테스트(나쁜 예):
내부 setCount 함수를 직접 테스트하거나 useState 후크의 내부 값을 직접 주장하려고 시도한다고 상상해 보세요. 현재 테스트 라이브러리로는 훨씬 더 어렵거나 불가능할 것이며, 상태 관리 방식을 리팩토링하기로 결정하면(예: useReducer로 전환) 즉시 실패할 것입니다.
이 원칙은 외부 서비스 또는 부모 컴포넌트와 상호 작용하는 컴포넌트에도 적용됩니다. 특정 인수로 fetch가 호출되었는지 주장하는 것(구현 세부 사항) 대신, 서비스를 모의 객체로 만들고 컴포넌트의 출력이 모의 데이터를 반영하는지(동작) 주장해야 합니다. 마찬가지로, 컴포넌트가 이벤트를 발생시키면 컴포넌트의 내부 이벤트 전송 메커니즘을 검사하는 것이 아니라, 모의 콜백을 제공하고 콜백이 호출되었는지 주장해야 합니다.
적용 시나리오
이 동작 테스트 접근 방식은 다양한 프론트엔드 시나리오에 적용할 수 있습니다.
- 양식 컴포넌트: 유효한 데이터로 양식을 제출하면 올바른 페이로드가 있는
onSubmitprop이 호출되고, 유효하지 않은 데이터로 제출하면 유효성 검사 메시지가 표시되는지 테스트합니다. 별도의 유틸리티로 노출되지 않는 한 내부의 유효성 검사 함수를 직접 테스트하지 마세요. - 탐색 컴포넌트: 링크를 클릭하면 예상 경로로 이동하거나
router.push함수가 호출되는지 테스트합니다. - 데이터 표시 컴포넌트: 특정 prop이 제공되면 컴포넌트가 올바른 형식으로 올바른 데이터를 렌더링하는지 테스트합니다.
- 대화형 위젯: 클릭 시 드롭다운이 열리고/닫히는 것, 선택 시 탭이 콘텐츠를 전환하는 것, 모달이 예상대로 나타나고/사라지는 것을 테스트합니다.
이 모든 경우에 초점은 사용자가 보고 상호 작용하는 것과 컴포넌트가 응답으로 생성하거나 변경하는 것에 있으며, 숨겨진 메커니즘이 아닙니다.
결론
내부 구현 세부 사항이 아닌 컴포넌트의 관찰 가능한 동작에 프론트엔드 테스트를 집중함으로써 강력하고 유지보수 가능하며 매우 효과적인 테스팅 전략을 개발할 수 있습니다. React Testing Library와 같은 라이브러리에 의해 자주 지원되는 이 접근 방식은 불안정한 테스트를 망가뜨릴 걱정 없이 자신 있게 코드를 리팩토링할 수 있도록 하여 궁극적으로 더 높은 품질의 애플리케이션과 더 즐거운 개발 경험으로 이어집니다. 사용자가 경험하는 것을 테스트하고, 구축 방법을 테스트하지 마세요.

