① 프론트엔드 테스트 코드 어디까지 작성해야 하나요? ② #react-test-code, #testing-library, #custom-hook #jest ③ 리액트 커스텀 훅 테스트 코드 작성
개발자에겐 땔래야 떌 수 없는 단어가 있죠. 바로 ‘테스트 코드’입니다. 이미 이 게시글을 들어오신 분들은 테스트 코드가 뭔지, 어떤 작용을 하는지 대략적으로 알고 계실 확률이 높습니다 🤣
테스트 코드를 작성하지 않더라도, 테스트 코드의 중요성을 부정하는 개발자는 아마 없을 겁니다. 많은 서적과 유명 셀럽 개발자들도 중요성을 얘기하고 있으니까요 (마틴파울러의 unitTest)
이렇게 중요하다고 입을 모아 말하는 ‘테스트 코드’ 어떻게 작성하면 좋을까요? 저는 프론트엔드 개발자이기 때문에 프론트엔드 시각에서 얘기해보려고 합니다.
테스트 코드와 커스텀훅 얘기를 하기 전에 제가 느낀 테스트 코드 필요성의 ‘동기’를 해보고자 합니다. 자바스크립트 프로젝트를 유지보수 하면서 일부 기능개발을 해야 하는 순간이 있었는데요. UpperComponent는 onClick을 ChildComponent에 props로 전달하고 있습니다.
<button>을 클릭을 하게 되면 props.onClick이 실행되어야 하는 코드입니다.
const UpperComponent = () => {
const onClick = () => {
// * **라우팅 이동**
};
return <ChildCommponent onClick={onClick} />;
};
const ChildCommponent = (props) => {
const onClickButton = () => {
// * ...내부 로직 처리...
props.onClick();
};
return (
<>
<button onClick={onClickButton}>다음</button>
</>
);
};
여기서 제가 겪은 무서운 경험은 props로 전해주는 onClick 함수가 라우팅 이동이 선언되지 않은 불완전한 상태로 배포가 되었습니다 😨 (다행히 큰일이 벌어지지는 않았습니다…)
문제를 빠르게 파악하고 호다닥 수정하여 배포를 한 경험이 있었는데, 이때 테스트 코드의 필요성을 느꼈습니다.
*** react 컴포넌트 내부에 선언되어 있는 함수를 테스트해야 한다.. 🙊 ***
테스트 코드라 하면 일반적으로 단위 테스트(unit test)를 떠올립니다. 일반적인 방법으로는 선언되어 있는 함수를 import 하여 다음과 같이 테스트 코드를 작성하게 됩니다.
<함수 선언>
// * src/util.ts
export const addNumber = (num1: number, num2: number) => {
return num1 + num2;
};
<테스트 코드 작성>
// * src/test/util.ts
import { addNumber } from '../util';
describe('addNumberTest', () => {
it('덧셈이 잘 되는지 확인한다.', () => {
expect(addNumber(10, 10)).toBe(20);
});
});
하지만, react의 컴포넌트는 비즈니스 로직이 있는 함수가 컴포넌트 내부에 공존하게 됩니다. useState와 같은 closure 도 같이 있기 때문에 순수 함수 이기 어렵습니다. 그렇다면 어떻게 테스트를 하면 좋을까요 🙊
예시로 되어있는 onClick 함수는 ‘라우팅 이동’이라는 단순한 예시 이기 때문에 분리하기엔 너무 작은 기능일 수 있으나, 큰 로직을 처리하게 되는 경우에는 분리하는 게 더 유용하다 느끼실 겁니다. 😄
const useOnClickHook = () => {
const onClick = () => {
// * 라우팅 이동
};
return {
onClick,
};
};
const UpperComponent = () => {
const { onClick } = useOnClickHook();
return <ChildCommponent onClick={onClick} />;
};
const ChildCommponent = (props) => {
const onClickButton = () => {
// * ...내부 로직 처리...
props.onClick();
};
return (
<>
<button onClick={onClickButton}>다음</button>
</>
);
};
onClick 함수를 리턴하는 커스텀 훅 (useOnclickHook)을 만들게 되면, onClick을 분리 할 수 있습니다. 오늘의 테스트 코드 작성 게시글의 핵심이 되는 키워드가 ‘커스텀 훅’입니다.
커스텀 훅을 만들게 되면 그 자체로 테스트 가능한 하나의 단위가 됩니다. 일반적으로 함수형 컴포넌트에서 useEffect를 선언하여 렌더 후에 이뤄지는 처리, 데이터의 변화 등을 다룰 수 있습니다. 여러 개의 useEffect 가 함수형 컴포넌트 내부에 선언되면 어떤 useEffect가 실행되는지, 이들간의 관계는 어떻게 되는지, 한눈에 들어오지 않기도 합니다. 만약 값을 공유하여 사용하고 있다면.. 벌써부터 머리가 아파오네요 😨
커스텀훅을 이용하여 useEffect를 분리하여 하나의 훅 안에서 하나의 useEffect가 실행되도록 나눠준다면 의미와 역할이 명확해집니다. 다시 얘기하면 테스트 코드를 작성하기 쉬워진다는 의미입니다 ✌🏻
이제 커스텀 훅을 테스트하는 테스트 코드를 만들어 보겠습니다. 간단한 역할부터 시작해 보겠습니다
// * src/useOnclickHook.tsx
// * useOnclickHook 을 별도 커스텀 훅 파일로 분리
export default function useOnclickHook() {
const navigate = useNavigate();
const onClick = () => {
navigate('/next');
};
return {
onClick,
};
}
// * useOnclickHook을 테스트 하는 JEST 테스트 코드
import { renderHook, act } from '@testing-library/react';
import { useOnclickHook } from './useOnclickHook'
describe('useOnclickHook', () => {
it('onClick 함수 확인', () => {
const { result } = renderHook(() => useOnclickHook());
const { onClick } = result.current;
act(() => onClick()); // * onClick 을 실행해준다.
expect(window.location).toBe('/next');
});
});
커스텀훅의 역할은 현재 주소가 로컬호스트 인지 아닌지 체크하여 isLocal 값을 리턴합니다.
import { useEffect, useState } from 'react';
export default function useLocalHostHook() {
const [isLocal, setIsLocal] = useState(false);
useEffect(() => {
if (window.location.hostname === 'localhost') {
setIsLocal(true);
} else {
setIsLocal(false);
}
}, []);
return {
isLocal,
};
}
// * useLocalHostHook 을 테스트하는 JEST 테스트 코드
import { renderHook } from '@testing-library/react';
import useLocalHostHook from './useLocalHostHook';
describe('useLocalHostHook', () => {
it('로컬인지 확인', () => {
const { result } = renderHook(() => useLocalHostHook());
const { isLocal } = result.current;
expect(isLocal).toBe(true);
});
});
간단하지만.. 이런 식으로, 커스텀훅을 테스트하는 코드를 작성할 수 있습니다. testing-library 에는 더 다양한 방법으로 테스트할 수 있도록 설명하고 있습니다! (제가 위에서 사용한 방식은 극히 기본적인 방법입니다)
커스텀훅으로 만들어진 함수를 테스트하는 것도 유닛 테스트보다는 큰 단위지만, 더 큰 컴포넌트도 테스트 코드를 작성해야 할 경우도 있습니다. 예를들면, react-query와 같은 API 호출이 있는 커스텀 훅, 페이지 전체를 렌더 하는 함수형 컴포넌트도 있습니다.
코드로 표현하기에는 내용이 너무 방대하고, 프로젝트의 성격상 일치하지 않을 수도 있어서 세팅 방법 링크로 대체하겠습니다 😭
‘테스트 코드’ 주제로도 정말 많은 얘기를 할 수 있지만, 오늘은 리액트 컴포넌트의 내부 함수와 useEffect를 별도의 커스텀 훅으로 분리하는 작업을 진행했습니다. 저는 처음에 useEffect를 분리하는 작업이 귀찮다고만 느껴지고 파일 개수가 늘어나고 복잡성이 늘어난다고 처음엔 느꼈습니다만, 컴포넌트 혹은 페이지의 크기가 커지고 어쩔 수 없이 useEffect의 선언 횟수가 많아질 때 커스텀 훅으로 분리하면서 읽기 쉽게 작성됨을 느꼈습니다. ✌🏻
추가로, 커스텀훅으로 뚝 떨어져 있는 것을 보고는 얼른 테스트 코드를 작성하고 싶어 졌습니다 🤣🤣🤣
분명히 리액트 컴포넌트를 테스트하는 방법 중 제가 말씀드린 방법 말고도 더 나은 방법이 있을 수 있습니다 🤩 youngjun.kim@genesislab.ai 메일로 피드백해주시면 더 나은 개발경험을 할 수 있을 것 같습니다. 🙏🏻
Write 김영준 (제네시스랩 서비스개발실 웹프론트개발팀)
Review & Edit 최성원 (제네시스랩 마케팅)