금주는 주중 출장으로 인해 주말에 과제를 모두 처리하고, 멘토링을 받는 오늘 배포과제 까지 완료했습니다.
과제를 진행하면서 직면한 문제는 환경 오류 2, 테스트 오류 1 이었습니다.
eslint와 prettier 충돌 오류generateItemsSpy 저장 시 충돌 문제는 함수의 파라미터에 들어오는 마지막 인자가 eslint의 설정에 의해 콤마가 없으면 에러라인이 발생하고, 저장시에는 prettier에 의해서 콤마가 삭제되어 저장되어 다시 에러라인이 발생하였습니다.
.prettier 에서 규칙을 통합하여 모두 콤마가 붙도록 수정하여 해결했습니다.
{
"trailingComma": "all"
}
커밋 시 주석이 지워져 다시 변경사항이 생기는 오류는 해결하지 못했습니다.
마지막으로 코드상의 문제는 먼저 GPT를 통해 답을 찾았지만, 정확한 로직을 이해하지 못해 멘토링 시간에 질문을 드렸었고, 정확한 답변을 받았습니다.
또한, 그 문제 또한 최적화 관련 의도였다고 확인하여 왜 해야 하는 지 까지 알게된 문제였습니다.
테스트 코드 중 vitest의 함수를 실행하는 빈도를 테스트하는 코드에서 generateItemsSpy가 1번만 호출되어야 하지만 2번 호출된다는 테스트 실패 문제였습니다.
테스트 코드
const renderLogMock = vi.spyOn(utils, "renderLog");
const generateItemsSpy = vi.spyOn(utils, "generateItems");
describe("최적화된 App 컴포넌트 테스트", () => {
beforeEach(() => {
renderLogMock.mockClear();
generateItemsSpy.mockClear();
});
it("여러 작업을 연속으로 수행해도 각 컴포넌트는 필요한 경우에만 리렌더링되어야 한다", async () => {
render(<App />);
renderLogMock.mockClear();
/* ... */
// 알림 닫기 버튼 찾기 및 클릭
await fireEvent.click(await screen.findByText("닫기"));
expect(renderLogMock).toHaveBeenCalledWith("NotificationSystem rendered");
expect(renderLogMock).toHaveBeenCalledWith("ComplexForm rendered");
expect(renderLogMock).toHaveBeenCalledTimes(2);
// HERE
expect(generateItemsSpy).toHaveBeenCalledTimes(1);
});
});
App.tsx
const AppContent = memo(() => {
const { theme } = useThemeContext();
// lazy initalizer 문제 >> advanced 마지막 테스트 gnerateItemsSpy
// const [items, setItems] = useState(generateItems(1000)); // 실패
const [items, setItems] = useState(() => generateItems(1000)); // 성공
const addItems = useCallback(() => {
setItems((p) => [...p, ...generateItems(1000, p.length)]);
}, []);
return (/* ... */)
}
일단 테스트 코드는 generateItems 함수를 실행하는 코드이고, 재랜더링 시 해당 함수가 호출이 되는 지를 확인하여 불필요한 함수 실행이 일어나는 지를 묻는 문제였습니다.
useState에 함수에 의한 결과 값을 지정하는 경우에는 컴포넌트가 재랜더 될 때, 해당 함수도 계속 다시 호출하여 불필요한 함수 실행이 이뤄지고, 아이템을 생성하는 갯수에 따라 성능이 끝도없이 늘어날 수 있었습니다.
다만, 해당 값은 함수를 실행한다고 하여 useState에 계속 반영되는 값이 아니어서 불필요한 함수 호출이 되는 것이었고, 이는 페이지가 바뀌면서 재랜더 되는 경우에 성능을 악화시켰습니다.
함수를 실행한 결과 값이 아닌, 함수를 useState에 넘기게 되면, 함수를 저장해두었다가 최초 랜더링시에 한번만 호출하여 사용하는 로직으로 변경됩니다.
// 1. 값을 반영
const [items, setItems] = useState(generateItems(1000));
// 랜더링 시 마다 값을 호출함
// 컴포넌트 호출 즉시 실행
const tmp = generateItems(1000); // <<
const [items, setItems] = useState(tmp);
// HERE >> useState에 값이 갱신되진 않지만 generateItems함수는 계속 호출하는 문제인 지?
// 2. 함수를 반영
const [items, setItems] = useState(() => generateItems(1000));
// 초기 랜더 시 1번만 실행함
// 함수를 저장해두었다가 초기 랜더 시에만 한번 실행되고,
// 그 결과값이 초기값이 됨
멘토링 간에 질문 드린 내용
멘토링 간에는 개발 이외에 항목에 대하여도 구체적으로 가르쳐주셨습니다.
해당 내용은 다른 부분에서 정리하도록 하고, 이번 과제를 진행하면서 라이브러리 React가 내부적으로 동작하는 원리를 기본 개념을 바탕으로 이해할 수 있었던 과정이었습니다.
회사에서 가끔 교육을 진행할 때가 있는데, React Hook에 대하여 교육을 진행하게 되면 해당 과제를 참조하면 좋은 교육 내용을 만들어 낼 수 있을 것 같다고 느껴졌던 과제였습니다.
또한, 과제를 일찍 끝냈던 만큼 오랜시간 고민하여 테스트 코드의 의도한 바와 문제점을 잘 찾아낼 수 있었던 것 같아 앞으로도 과제를 최대한 빨리 완료하고, 문제가 되는 부분 위주로 해결방법과 원리를 익히는 데 시간을 활용하면 더 좋은 결과를 보일 것이라고 생각되었습니다.