ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์ ํ ์คํธ ์ฝ๋ ์์ฑ ์ ํ์ํ ๋ค์ํ ํ ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํจ์๋ค๊ณผ ๊ฐ๋ ์ ์ ํํ ์ดํดํ๊ณ ํ์ฉํด๋ณด์.
์ค์ ๋ก Auto-Summary-AI ํ๋ก์ ํธ๋ฅผ ๊ฐ๋ฐํ๋ฉด์ ํ ์คํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฒ์๋ถํฐ ๋๊น์ง ์ ์ฉํด๋ณด๋ ๊ฒฝํ์ ํ๋ค. ๋ ธํธ ์์ฑ, ํธ์ง, AI ์์ฝ ๊ธฐ๋ฅ๊น์ง ํฌํจ๋ ๋ณต์กํ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ ํ ์คํธ ์ฝ๋๋ฅผ ์์ฑํ๋ฉด์ ๋ง์ ์ํ์ฐฉ์ค์ ๊นจ๋ฌ์์ ์ป์ ์ ์์๋ค.
๊ด๋ จ ํ๋ก์ ํธ: Auto-Summary-AI
์ค์ ํ๋ก์ ํธ์์ ํ ์คํธ๋ฅผ ๊ตฌํํ๋ฉด์ ๋ถ๋ชํ ์ฃผ์ ๊ณผ์ ๋ค:
ํ ์คํธ ํ๊ฒฝ์ ํ๊ณ๋ฅผ ๊ทน๋ณตํ๊ธฐ ์ํ ๋ค์ํ ๋ชจํน ๊ธฐ๋ฒ์ ํ์ฉํ๋ค.
๋ณต์กํ ๊ธฐ๋ฅ์ ์ฒด๊ณ์ ์ผ๋ก ํ ์คํธํ๊ธฐ ์ํ ๊ตฌ์กฐ ์ค๊ณ
์ค์ ๋ธ๋ผ์ฐ์ ์์ด๋ ๋ผ์ฐํ ์ ์๋ฒฝํ๊ฒ ํ ์คํธํ ์ ์๋ ๋ฐฉ๋ฒ ๊ตฌ์ถ
๊ฐ์ฅ ํฐ ๋๊ด: RTK + redux-persist ์กฐํฉ์์์ localStorage ๋ฌธ์
ํ๋ก์ ํธ์์ ์ฌ์ฉ์์ ๋ ธํธ๋ฅผ ๋ธ๋ผ์ฐ์ ์ ์ ์ฅํ๊ธฐ ์ํด redux-persist๋ฅผ ํ์ฉํ๋๋ฐ, ํ ์คํธ ํ๊ฒฝ์์๋ ์ค์ ๋ธ๋ผ์ฐ์ ํ๊ฒฝ์ด ์๋๋ผ์ localStorage๋ฅผ ์ฌ์ฉํ ์ ์๋ค๋ ๊ฑธ ๋ค๋ฆ๊ฒ ๊นจ๋ฌ์๋ค. ๐
ํด๊ฒฐ ๊ณผ์ ์์ ๊ฐ์ฅ ํท๊ฐ๋ ธ๋ ๋ถ๋ถ: getItem์์ Promise.resolve(null)์ ๋ฐํํด์ผ ํ๋ค๋ ์ ์ด์๋ค. setItem/removeItem์ ๋จ์ํ ๋์๋ง ๋ชจํนํ๋ฉด ๋์ง๋ง, getItem์ ์ค์ ๋ก ๊ฐ์ ๋ฐํํ๋ ํจ์์ด๊ธฐ ๋๋ฌธ์ ์ด๊ธฐ ์ํ์์๋ null์ ๋ฐํํ๋๋ก ์ค์ ํด์ผ ํ๋ค๋ ๊ฑธ ์ํ์ฐฉ์ค๋ฅผ ํตํด ํ์ตํ๋ค.
// ๋ธ๋ผ์ฐ์ ์ ์ญ ํจ์ ๋ชจํน (์๋ฆผ ๊ธฐ๋ฅ ํ
์คํธ์ฉ)
window.alert = vi.fn();
// HTTP ํด๋ผ์ด์ธํธ ์ ์ฒด ๋ชจํน
vi.mock("axios");
// OpenAI API ์๋ต ๋ชจํน - ์ค์ API ์๋ต ๊ตฌ์กฐ ๊ทธ๋๋ก ์ฌํ
vi.spyOn(api, "fetchOpenAI").mockResolvedValueOnce({
choices: [{ message: { content: "AI๊ฐ ์์ฑํ ์์ฝ ๋ด์ฉ" } }]
});
// redux-persist์ฉ ์คํ ๋ฆฌ์ง ๋ชจํน (ํต์ฌ ํฌ์ธํธ!)
const mockStorage = {
getItem: vi.fn(() => Promise.resolve(null)), // ์ด๊ธฐ๊ฐ null ๋ฐํ์ด ํต์ฌ
setItem: vi.fn(() => Promise.resolve()), // ์ ์ฅ ๋์๋ง ๋ชจํน
removeItem: vi.fn(() => Promise.resolve()) // ์ญ์ ๋์๋ง ๋ชจํน
};
ํ๋ก์ ํธ ํน์ฑ: ๊ฐ์ ๊ธฐ๋ฅ์ ๋ฒํผ์ด ์ฌ๋ฌ ๊ณณ์ ๋ฐฐ์น๋จ (ํค๋, ์ฌ์ด๋๋ฐ ๋ฑ)
// ์ค์ ์ฌ์ฉ ํจํด: ๊ตฌ์กฐ๋ถํดํ ๋น์ผ๋ก ํ์ํ ์ฟผ๋ฆฌ๋ง ์ถ์ถ
const {getAllByText, getByTestId, getByText, getAllByTestId} = render(
<Provider store={testStore}>
<PersistGate loading={<div data-testid="loading">Loading...</div>} persistor={persistor}>
<RouterProvider router={router}/>
</PersistGate>
</Provider>
);
// PersistGate ๋ก๋ฉ ์๋ฃ๊น์ง ๋๊ธฐ (redux-persist ํน์ฑ์ ํ์)
await waitFor(() => {
expect(screen.queryByTestId("loading")).not.toBeInTheDocument();
}, { timeout: 3000 });
// Code Splitting์ผ๋ก ์ธํ lazy loading ๋๊ธฐ
await waitFor(() => {
expect(screen.queryByText("loading...")).not.toBeInTheDocument();
}, { timeout: 5000 });
์ค์ ์ฌ์ฉ์ ํ๋ก์ฐ: ๋ ธํธ ์์ฑ ๋ฒํผ ํด๋ฆญ โ ํ์ด์ง ์ด๋ โ ๋ด์ฉ ์ ๋ ฅ โ ์์ฝ ์์ฑ โ ์ ์ฅ
// ํ์ค์ ์ธ ๋ฌธ์ : "๋
ธํธ ์์ฑ" ๋ฒํผ์ด ์ฌ๋ฌ ๊ฐ ์กด์ฌ
const noteButtons = getAllByText("๋
ธํธ ์์ฑ");
expect(noteButtons).toHaveLength(2); // ํค๋์ ์ฌ์ด๋๋ฐ์ ๊ฐ๊ฐ ์กด์ฌ
await userEvent.click(noteButtons[0]); // ์ฒซ ๋ฒ์งธ(ํค๋) ๋ฒํผ ํด๋ฆญ
// ์ค์ ์ฌ์ฉ์์ฒ๋ผ ๊ธฐ์กด ๋ด์ฉ ์ง์ฐ๊ณ ์๋ก ์
๋ ฅ
const titleEl = getByTestId('title');
await userEvent.clear(titleEl);
await userEvent.type(titleEl, "ํ
์คํธ์ฉ ์ ๋ชฉ");
// textarea๊ฐ ์ฌ๋ฌ ๊ฐ์ธ ๊ฒฝ์ฐ (์๋ณธ ๋ด์ฉ, ์์ฝ ๋ด์ฉ)
const contentEl = getAllByTestId('content');
await userEvent.clear(contentEl[0]);
await userEvent.type(contentEl[0], "30์ ์ด์์ ์ถฉ๋ถํ ๊ธด ํ
์คํธ ๋ด์ฉ์
๋๋ค. AI ์์ฝ์ ์ํด ์์ฑํ ์์ ํ
์คํธ์
๋๋ค.");
ํ๋ก์ ํธ ํน์ง: UUID ๊ธฐ๋ฐ ๋์ ๋
ธํธ ํ์ด์ง (/notes/12345678-1234-1234-1234-123456789abc)
// ๋
ธํธ ์์ฑ ํ ์๋์ผ๋ก ํด๋น ๋
ธํธ ํ์ด์ง๋ก ์ด๋๋๋์ง ํ์ธ
await waitFor(() => {
expect(router.state.location.pathname).toMatch(/^\/notes\/[a-f0-9-]{36}$/)
});
// ๋
ธํธ ์ ์ฅ ํ ํ์ผ๋ก ๋ฆฌ๋ค์ด๋ ํธ๋๋์ง ํ์ธ
await waitFor(() => {
expect(router.state.location.pathname).toBe('/')
});
RTK์ ์ฅ์ ํ์ฉ: testStore.getState()๋ก ์ํ ์ง์ ์ ๊ทผ
// ๋
ธํธ๊ฐ ์ค์ ๋ก Redux ์คํ ์ด์ ์ ์ฅ๋์๋์ง ํ์ธ
await waitFor(() => {
const notes = testStore.getState().notes.lists;
expect(notes).toHaveLength(1);
expect(notes[0].title).toBe("ํ
์คํธ์ฉ ์ ๋ชฉ");
expect(notes[0].content).toBe("30์ ์ด์์ ์ถฉ๋ถํ ๊ธด ํ
์คํธ ๋ด์ฉ์
๋๋ค...");
expect(notes[0].summary).toBe("AI๊ฐ ์์ฑํ ์์ฝ ๋ด์ฉ");
});
ํ๋ก์ ํธ์ ํต์ฌ ์ ํจ์ฑ ๊ฒ์ฌ: 30์ ๋ฏธ๋ง์ ์์ฝ ๋ถ๊ฐ
// ๋น์ฆ๋์ค ๋ฃฐ์ ๋ฐ๋ฅธ ๋ถ๊ธฐ ์ฒ๋ฆฌ
if (contentEl[0].value.length < 30) {
expect(window.alert).toHaveBeenCalledWith('์์ฝํ๋ ค๋ฉด ์ต์ 30์ ์ด์ ์
๋ ฅํด์ฃผ์ธ์.');
} else {
expect(window.alert).not.toHaveBeenCalledWith('์์ฝํ๋ ค๋ฉด ์ต์ 30์ ์ด์ ์
๋ ฅํด์ฃผ์ธ์.');
}
// ์์ฝ ์์ฑ ์ฌ๋ถ์ ๋ฐ๋ฅธ ์ ์ฅ ๊ฐ๋ฅ์ฑ ๊ฒ์ฆ
if (window.alert.mock.calls.length > 0) {
expect(window.alert).toHaveBeenCalledWith('์์ฝ ๋ฒํผ์ ํด๋ฆญํ์ฌ ์์ฝ ๊ฒฐ๊ณผ๋ฅผ ์ ๋ฆฌํด์ฃผ์ธ์.');
} else {
// ๋ชจ๋ ๊ฒ์ฆ์ด ํต๊ณผํ ๊ฒฝ์ฐ์ ์ฑ๊ณต ์๋๋ฆฌ์ค
}
vi.fn() vs vi.mock()์ ๋ช ํํ ์ฐจ์ด: ๊ฐ๋ณ ํจ์๋ vi.fn(), ๋ชจ๋ ์ ์ฒด๋ vi.mock()์ผ๋ก ์ฒ๋ฆฌ
createMemoryRouter์ ํต์ฌ ๊ฐ์น: ๋ธ๋ผ์ฐ์ ํ๊ฒฝ ์์ด๋ ์์ ํ ๋ผ์ฐํ ํ ์คํธ ๊ฐ๋ฅ
์ค์ ๊ฒฝํ: input/textarea๋ toHaveValue, ์ผ๋ฐ ํ
์คํธ ์์๋ toHaveTextContent
Redux + Router + PersistGate: ๊ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ ์ด๊ธฐํ ์์์ ๋ฐฉ๋ฒ์ด ํ ์คํธ ์ฑ๊ณต์ ํต์ฌ
toMatch() ํ์ฉmock.calls.length๋ก ํจ์ ํธ์ถ ์ฌ๋ถ ํ๋จ ํ ๋ถ๊ธฐbeforeEach/afterEach ํฉ๊ธ ์กฐํฉ:
persistor.purge()๋ก ์์ ์ํ๊น์ง ์์ ์ด๊ธฐํ๊ตฌ์กฐ๋ถํดํ ๋น์ ์๋ ฅ: render ๋ฐํ๊ฐ์์ ์ค์ ์ฌ์ฉํ ํจ์๋ค๋ง ์ ๋ณ์ ์ผ๋ก ์ถ์ถ
์ํฉ๋ณ ํ์์์ ์ค์ :
์กฐ๊ฑด๋ถ ํ ์คํธ์ ํ: ์ฑ๊ณต/์คํจ ์ผ์ด์ค๋ฅผ ๋ชจ๋ ๊ณ ๋ คํ ๋ถ๊ธฐ ์ฒ๋ฆฌ๋ก ๊ฒฌ๊ณ ํ ํ ์คํธ