-
์ํฐ๋ ํ๋ฆฌ์จ๋ณด๋ฉ ํ๋ก ํธ์๋ ์ฝ์ค 3์ฃผ์ฐจ ํ๊ณ (1) — Next.jsETC 2022. 3. 22. 02:29
ํ๋ฆฌ์จ๋ณด๋ฉ์ ์์ํ๊ณ ๋๋ ์๊ฐ์ด ๋ฌด์ง ๋น ๋ฅด๊ฒ ํ๋ฅธ๋ค. ๋๋์ด 3์ฃผ์ฐจ! ์ด๋ฒ์๋
Next.js
๋ฅผ ์ฒ์ ์จ ๋ณด๊ฒ ๋์๋ค. ์ด๋ฆ๋ง ๋ค์์ ๋ ์ด๋ ค์ธ ๊ฒ ๊ฐ์๋ Next.js๋ ๋ง์ ํด ๋ณด๋ ์๊ฐ๋ณด๋ค ์ด๋ ต์ง๋ ์์์ง๋ง, ์ญ์ ์ฒ์์ด๋ผ ๊ทธ๋ฐ์ง ์ด๋ ต๊ธด ์ด๋ ค์ ๋ค.(๋ชจ์๋ฉ์ด๋ฆฌ)์ฌ์ค ๋ง๊ฐ ๊ธฐํ๊น์ง๋ ์๋ฒฝํ๊ฒ ์ดํดํ์ง ๋ชปํ๋๋ฐ ํ๋ฆฌ์จ๋ณด๋ฉ ์ฝ์ค๊ฐ ๋๋๊ณ ๋ณต์ตํ๊ณ ๋์์ผ ์กฐ๊ธ์ฉ ์ดํดํ๋ ์ค์ด๋ค....์ด๋ฒ ๊ณผ์ ์์ ๋๋ ์ํ ์์ธ ํ์ด์ง, ๊ณ ๊ฐ์ผํฐ์ ๋ง์ดํ์ด์ง, e2e ํ ์คํธ๋ฅผ ๋งก์๋ค.
์๋ฌดํผ ์์~
1. ์ํ ์์ธ ํ์ด์ง
์ฒ์์๋ ๋ฆฌ์กํธ์์ components๋ฅผ ์์ฑํ๋ ๊ฒ๊ณผ ๋์ผํ๊ฒ components ํด๋ ๋ด์ ํ์ด์ง๋ง๋ค ํด๋๋ฅผ ๋๋ ์ ์ปดํฌ๋ํธ๋ฅผ ์์ฑํ๋ค.
์ปดํฌ๋ํธ ๋ถ๋ฆฌ ๊ธฐ์ค์ ์ ์ฌ์ง๊ณผ ๊ฐ์ด ๋ถ๋ฆฌํ๋ค.
OptionItem์์ ์ ํจ๊ธฐ๊ฐ์ ํ์ํ๋ ๋ก์ง, ํ ์ธ์จ์ ๊ตฌํ๋ ๋ก์ง, ๊ฐ๊ฒฉ์ ์ธ ์๋ฆฌ๋ง๋ค ์ฝค๋ง๋ฅผ ์ฐ๋ ๋ก์ง์ ๋ฐ๋ก ๋ถ๋ฆฌํด utils ํด๋์ ๋ค์๊ณผ ์์ฑํ๋ค.
export const getSaleRate = ( originalPrice: number, sellingPrice: number, ): number => { return Math.round(((originalPrice - sellingPrice) / originalPrice) * 100); }; export const getExpiryDate = (date: string): string => { if (date.length === 24) { const dateArray: string[] = date.substring(0, 10).split('-'); return `${dateArray[0]}.${dateArray[1]}.${dateArray[2]}`; } else { const dateArray: string[] = date.split(' '); const month = getMonth(dateArray[1]); const day = dateArray[2]; const year = dateArray[3]; return `${year}.${month}.${day}`; } }; export const get3DigitsCost = (cost: number): string => { return cost.toLocaleString(); };
ํ์ง๋ง ๋ฐ๋ก ๋ถ๋ฆฌํด ์์ฑํ ๋ก์ง์ ๋ฒ๊ทธ๊ฐ ์์๋ค. ์ ํจ๊ธฐ๊ฐ์ ํ์ํ๋ getExpiryDate ํจ์์ ์ซ์์ ๊ฒฝ์ฐ 3์๋ฆฌ๋ง๋ค ์ฝค๋ง๋ฅผ ์ฐ๋ get3DigitsCost ํจ์์์ ๋ฒ๊ทธ๊ฐ ๋ฐ์ํ๋ค. ๋ฐ๋ผ์ ๋ฆฌํฉํ ๋ง ๊ณผ์ ์ ๊ฑธ์ณ ๋ฒ๊ทธ๋ฅผ ์์ ํ๋ค.
getExpiryDate ํจ์์์๋ 2022.03.20 ํ์์ด ์๋ undefined.undefined.undefined ํ์์ผ๋ก ๊ฐ์ด ๋ฆฌํด๋๋ ๋ฒ๊ทธ๊ฐ ์์๋ค. ์ด์ ๋ API์์ ๊ฐ์ ๋ฐ์ ์์ ๋ 2023-03-20T00:00:00+09:00 / 2022-03-20T15:00:00.000Z ๋ ๊ฐ์ง ํ์์ ๊ฐ์ ๋๋คํ๊ฒ ๋ฐ์๊ธฐ ๋๋ฌธ์ด๋ค. (์ฃผ๋ก ํ์ด์ง๋ฅผ ์๋ก๊ณ ์นจ ํ์ ๋ ํ์์ ํ์์ผ๋ก ๊ฐ์ด ๋์ด์์) ์ฒ์์๋ string์ ๊ฐ๊ณตํ์ฌ ๋ฆฌํด ๊ฐ์ ๋ง๋ค๋ ค๊ณ ํ์ผ๋, API์์ ๋๊ฒจ ์ค ํ์์ Date ํจ์๋ฅผ ์ด์ฉํ๋ค๋ฉด ์ฝ๊ฒ ๊ฐ๊ณตํ ์ ์๋ค๋ ๊ฒ์ ์๊ฒ ๋์ด ๋ค์๊ณผ ๊ฐ์ด ์์ ํ๋ค.
export const getExpiryDate = (input_date: string): string => { const date = new Date(input_date); const year = date.getFullYear(); const month = date.getMonth() + 1; const day = date.getDate(); return `${year}.${month > 10 ? month : `0${month}`}.${ day > 10 ? day : `0${day}` }`; };
โ
๋ ๋ฒ์งธ๋ก get3DigitsCost ํจ์์์ ์ฝค๋ง๊ฐ ์ฐํ์ง ์๋ ๋ฒ๊ทธ๋ get3DigitsCost ํจ์์ ์ธ์๋ก string ํ์ ์ด ์ ๋ฌ๋๋ค๋ฉด string ํ์ ์ number๋ก ๋ฐ๊พธ๋๋ก ์์ ํด ํด๊ฒฐํ ์ ์์๋ค.
export const get3DigitsCost = (cost: number | string): string => { return typeof cost === 'string' ? Number(cost).toLocaleString() : cost.toLocaleString(); };
2. Redux ์ฌ์ฉํ๊ธฐ
Next.js์์ Redux๋ฅผ ์ฌ์ฉํ ๋๋ ์์ ๋ฆฌ์กํธ๋ก ์์ฑํ์ ๊ฒฝ์ฐ์ ๋ฌ๋ฆฌ ์ถ๊ฐ ์ค์ ์ ๊ฑฐ์ณ์ผ ํ๋ค๊ณ ํ๋ค.
npm i next-redux-wrapper
์ฐ์ next-redux-wrapper ํจํค์ง๋ฅผ ์ถ๊ฐ๋ก ์ค์นํด ์คฌ๋ค.
// store/index.ts import { configureStore } from '@reduxjs/toolkit'; import { createWrapper } from "next-redux-wrapper"; const makeStore = () => configureStore({ reducer: { fetch: fetchDataSlice.reducer, brand: selectBrandSlice.reducer, option: optionSlice.reducer, category: categoryIdSlice.reducer, mypage: mypageSlice.reducer, }, }); export const wrapper = createWrapper(makeStore, { debug: process.env.NODE_ENV !== 'production', });
์์ฑํ ๋ฆฌ๋์๋ค์ ๋ชจ์ store๋ฅผ ์์ฑํ ๋ค, ์์ฑ๋ ์คํ ์ด๋ฅผ createWrapper์ ๋ด์ wrapper๋ฅผ ์์ฑํด ์ค์ผ ํ๋ค.
// pages/_app.tsx import type { AppProps } from 'next/app'; import { wrapper } from 'store'; const MyApp: React.FC<AppProps> = ({ Component, pageProps }) => { return <Component {...pageProps} /> }; export default wrapper.withRedux(MyApp);
๊ทธ๋ฆฌ๊ณ wrapper์ withRedux HOC์ ์ฌ์ฉํด App ์ปดํฌ๋ํธ๋ฅผ ๊ฐ์ธ ์ค๋ค. ๊ทธ๋ฌ๋ฉด ๊ฐ ์ปดํฌ๋ํธ์ getInitialProps, getServerSideProps, getStaticProps์์ store์ ์ ๊ทผํ ์ ์๋ค๊ณ ํ๋ค.
๋ฆฌ๋์ค๋ฅผ ์ธํ ํ ๋ ์ฐธ๊ณ ํ ๊ธ๋ค์ HYDRATE๋ผ๋ ์ก์ ์ ํตํด ์๋ฒ์ ํด๋ผ์ด์ธํธ์ ์ํ๋ฅผ ํฉ์น๋ ์์ ์ ์ํํ ์ ์๋ค๋ ์ค๋ช ๋ ์์๋๋ฐ, ์๋ฒฝํ๊ฒ ์ดํด๊ฐ ๋์ง ์์ ํ ๋ฒ ๋ ์ค์ ์ ํด ๋ด์ผ ํ ๊ฒ ๊ฐ๋ค.
3. ๊ณ ๊ฐ์ผํฐ์ ๋ง์ดํ์ด์ง
์ฐ๋ฆฌ ํ์ ํ๋ก์ ํธ๋ฅผ ๋๋ ์ ์งํํ ๋ ๋ฉ์ธ ํ์ด์ง, ์นดํ ๊ณ ๋ฆฌ ํ์ด์ง, ๋ธ๋๋ ํ์ด์ง, ์ํ ์์ธ ํ์ด์ง๋ฅผ ๋๋๊ณ ์์ํด์ ๊ณ ๊ฐ์ผํฐ์ ๋ง์ดํ์ด์ง๊ฐ ๋จ๋ ์ํฉ์ด์๋ค. ์ด๋ป๊ฒ ํ ๊น ์ ํ๋ค๊ฐ ์ฐ๋ฆฌ ํ์ ๊ฐ์ฅ ์ด๋ ค์ด ๋ถ๋ถ๋ค ๋๋งก์ ํ๊ณ ์ถ์ด ํ๋ ์ด์ ์ ์ธ ์ฌ๋๋ค๋ง ๋ชจ์ฌ ์๋ ํ์ด๋(๋ค๋ค ์กด๊ฒฝ...) ๋จผ์ ํ ๋น๋์ ์ด๋ ์ ๋ ๋๋ธ ์ฌ๋์ด ๊ณ ๊ฐ์ผํฐ์ ๋ง์ดํ์ด์ง๋ฅผ ์์ ํ์๊ณ ์๊ธฐ๋ฅผ ํด ๋์ ์ํ์๋ค. ๊ทธ๋ฆฌ๊ณ ๋ด ์์ ์ด ์ ์ผ ๋จผ์ ๋๋ฌ๋ค. ๊ทธ๋์ ๊ณ ๊ฐ์ผํฐ๋ ๋ง์ดํ์ด์ง ์ ๊ฐ ํ ๊ฒ์~ ํ๋ค.
๊ณ ๊ฐ์ผํฐ
๊ณ ๊ฐ์ผํฐ๋ ์ ์ด๋ฏธ์ง์ ๊ฐ์ด ๊ตฌ๋งค ํญ๊ณผ ํ๋งค ํญ์ด ๋๋์ด์ ธ ์๊ณ , ๊ตฌ๋งค ํญ๊ณผ ํ๋งค ํญ์ ๊ฐ๊ฐ ํด๋ฆญํ๋ฉด ํ๋จ์ ์ง๋ฌธ ๋ชฉ๋ก์ด ํด๋ฆญํ ํญ์ ๋ง๊ฒ ๋ฐ๋์ด์ผ ํ๋ค. ๊ทธ๋ฆฌ๊ณ ๋๋ ์ฒ์์ ํญ์ ํด๋ฆญํ๋ฉด ๊ฐ ํญ์ ํด๋นํ๋ qna ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ํ๋ฉด์ด ๋ฆฌ๋ ๋๋ง ๋๋ ํ์์ผ๋ก ๊ตฌํํ๋ ค๊ณ ํ๋ค. ๊ทธ๋ฌ๋ ๋ด ๋ง์๊ณผ ๊ฐ์ง ์์์.... ๐ฅฒ ๊ณ ๊ฐ์ผํฐ์ ๋ผ์ฐํ ์ ๊ตฌ๋งค / ํ๋งค๊ฐ ๋๋์ด์ ธ ์๋ ํ์์ด ์๋ ๋์ผํ endpoint ๋ด์์ ํ๋ฉด๋ง ๋ฐ๋๋ ํ์์ด์๋ค. ๊ทธ๋ฌ๋ CSR๋ก ๊ตฌํํ์ ๊ฒฝ์ฐ์๋ ์ด๊ธฐ์ fetch ํด ์ค๋ ๊ตฌ๋งค ํญ์ ๋ฐ์ดํฐ๋ง ๋ฐ์ ์ฌ ์๋ฐ์ ์์์ ๊ฒ์ด๋ค.
๋ฐ๋ผ์ ์ด ๋ถ๋ถ์ ํ๋จ์ ์ฝ๋์ฒ๋ผ QnaList ์ธก์์ ๊ตฌ๋งค์ ํ๋งค ๋ฐ์ดํฐ๋ฅผ ๋ฏธ๋ฆฌ fetch ํด ๋๊ณ , ํญ ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํ๋ฉด ๊ทธ์ ๋ง๋ ๋ฐ์ดํฐ๋ฅผ ๋ ๋๋งํ๋๋ก ๊ตฌํํ๋ค.
import { useState } from 'react'; import useAxios from 'hooks/useAxios'; import { Qas } from 'types/qaTypes'; import Qna from './Qna'; const QnaList = ({ qaId }: { qaId: number }) => { const qaBuyList = useAxios<Qas>(`qas?qaTypeId=1`); const qaSellList = useAxios<Qas>(`qas?qaTypeId=2`); const [currentQa, setCurrentQa] = useState<number>(0); if (!qaBuyList || !qaSellList) return <div>๋ก๋ฉ์ค</div>; return ( <section> <div> {qaId === 1 ? qaBuyList.qas.map((qa) => ( <Qna key={qa.id} qa={qa} currentQa={currentQa} setCurrentQa={setCurrentQa} /> )) : qaSellList.qas.map((qa) => ( <Qna key={qa.id} qa={qa} currentQa={currentQa} setCurrentQa={setCurrentQa} /> ))} </div> </section> ); }; export default QnaList;
๊ทธ๋ฆฌ๊ณ ๋ ๋์๊ฐ CSR๋ก ํญ์ ํด๋ฆญํ์ ๋ QnaList๊ฐ ๋ฐ๋๋๋ก ๊ณ ์ณ์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ์ ์์์ง๋ง, ์๋ก๊ณ ์นจ์ ํ์ ๋ 404 ์ค๋ฅ๊ฐ ๋๋ ์ํฉ์ด ๋ฐ์ํ๋ค. ๋ฐ๋ผ์ ์ด ๋ถ๋ถ์ ๊ตฌ๋งค ํญ๊ณผ ํ๋งค ํญ์ QnaList๋ฅผ ๋ฏธ๋ฆฌ fetch ํด ๋๋ ๊ฒ ํ๋ช ํ ๊ฒ์ด๋ผ๊ณ ํ๋จํ๋ค. (๋ฌผ๋ก ํญ์ด ๋ง์์ง ๊ฒฝ์ฐ์๋ ๋นํจ์จ์ ์ด๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ผ๋ก ๊ตฌํํด์ผ๊ฒ ์ง๋ง ํ์ฌ๋ ๋ฐ์ดํฐ๊ฐ ๋ช ๊ฐ ์๋ ์ํฉ์ด๊ธฐ ๋๋ฌธ์!)
๋ํ, ํ๋ก์ ํธ๋ฅผ ํ ๋น์์๋ Next.js์ ๋๋ฌด ๋งจ๋ ์ ํค๋ฉ์ ํ๋ ํฐ๋ผ getServerSideProps์ ์ฌ์ฉ๋ฒ์ ์ ๋๋ก ์์ง ๋ชปํ๋ค.
๊ทธ๋์ Next.js๋ฅผ ์ผ๋ค๋ ๊ฒ์ ์์๊ฐ ์์ง๋ง ์์ฌ์์ด ๋จ๋ ํ๋ก์ ํธ์๋๋ฐ, ์ดํ ๋ฆฌํฉํ ๋ง์ ํ๋ฉด์
getServerSideProps
๋ฅผ ์ฌ์ฉํด ๋ฆฌํฉํ ๋ง์ ์งํํ๊ณ , ๊ฐ ์ปดํฌ๋ํธ ๋ด์์ ๋ฐ์ดํฐ๋ฅผ fetch ํด ์ค๋ ๋ก์ง์ ๋ค์๊ณผ ๊ฐ์ด SSR๋ก ์ฒ๋ฆฌํ ์ ์๋๋ก ๋ณ๊ฒฝํ๋ค. Contact ์ธ์๋ ๋ธ๋๋ ํ์ด์ง, ์นดํ ๊ณ ๋ฆฌ ํ์ด์ง์์๋getServerSideProps
๋ฅผ ์ฌ์ฉํ์๋ค.// pages/contacts/index.ts // ๋ณ๊ฒฝ ์ import Contact from 'components/Contact'; import React from 'react'; const Contacts = () => { return <Contact />; }; export default Contacts;
// pages/contacts/index.ts // ๋ณ๊ฒฝ ํ import axios from 'axios'; import Contact from 'components/Contact'; import React from 'react'; import { ContactType } from 'types/index'; const Contacts = ({ qaTypes, qaBuyList, qaSellList }: ContactType) => { return ( <Contact qaTypes={qaTypes} qaBuyList={qaBuyList} qaSellList={qaSellList} /> ); }; export async function getServerSideProps() { const { data: qna } = await axios.get('/qa-types'); const { data: qaBuy } = await axios.get('/qas?qaTypeId=1'); const { data: qaSell } = await axios.get('/qas?qaTypeId=2'); const qaTypes = qna.qaTypes; const qaBuyList = qaBuy.qas; const qaSellList = qaSell.qas; return { props: { qaTypes, qaBuyList, qaSellList } }; } export default Contacts;
๋ง์ดํ์ด์ง
๊ณ ๊ฐ์ผํฐ๋ฅผ ๋จผ์ ์์ ํ ํ, ๋ฉ์ธ์์ ๊ณ ๊ฐ์ผํฐ๋ก ์ ์ํ ์ ์๋๋ก ๋ฉ์ธ์ ํ๋ฒ๊ฑฐ ๋ฉ๋ด๋ฅผ ๋๋ฅด๋ฉด ๋ง์ดํ์ด์ง ๋ฉ๋ด๊ฐ ๋์ค๊ฒ ๊ตฌํํด์ผ ํ๋ค.
ํ๋ฉด์ ๋ณ๊ฒ ์์ด์ ์ฌ์ ์ผ๋ ์ ๋๋ฉ์ด์ ๋ถ๋ถ์์ ์ ๋ฅผ ๋จน์๋ค. ์ฒซ ๋ฒ์งธ ๋ฌธ์ ๋ ์ฐ๋ฆฌ๋ ๋ฐ์คํฌํ๊ณผ ๋ชจ๋ฐ์ผ ๋ทฐ๋ฅผ ๋ฐ๋ก ๊ตฌํํ๋ ๊ฒ ์๋๋ผ ๋ฐ์คํฌํ ํ๋ฉด ๋ด์์ ๋ชจ๋ฐ์ผ ๋น์จ๋ก ๋ณด์ผ ์ ์๊ฒ๋ ์ ์ฒด ์ปดํฌ๋ํธ๋ฅผ ๋ด๊ณ ์๋ div์ ๋ ์ด์์์ ์ค์ ํ๋๋ฐ, ๋ง์ดํ์ด์ง๊ฐ ์ค๋ฅธ์ชฝ์ผ๋ก ์ฌ๋ผ์ด๋๋๋ ์ ๋๋ฉ์ด์ ์ ์ ์ฉํ๋ ์ ์ฒด div์ ์์ชฝ์์ ๋ ์์ค๋ ๊ฒ์ฒ๋ผ ๋ณด์๋ค. ๊ทธ๋ฆฌ๊ณ ๋ ๋ฒ์งธ ๋ฌธ์ ๋ ๋ง์ดํ์ด์ง๊ฐ ์ด๋ฆด ๋๋ ์ ๋๋ฉ์ด์ ์ด ๋์ํ์ง๋ง, ๋ซํ ๋๋ ์ ๋๋ฉ์ด์ ์์ด ๊ทธ๋ฅ ์์ด์ง๋ค๋ ๊ฑฐ์๋ค.
์ด ๋ถ๋ถ์ ๋ค๋ฅธ ํ์ ๋ถ์ด ์๋ด์ฃผ์ ์ ํด๊ฒฐํ ์ ์์๋ค. (๊ฐ์ฌํฉ๋๋ค ๐ญ)
์ฒซ ๋ฒ์งธ ๋ฌธ์ ๋ ์ ์ฒด div์
overflow-x: hidden
์์ฑ์ ์ถ๊ฐํด์ ํด๊ฒฐํ๋ค. ๋ ๋ฒ์งธ ๋ฌธ์ ๋, ๋ซ๊ธฐ ๋ฒํผ์ ํด๋ฆญํ๋ฉดdisplay: none
์์ฑ์ ์ฌ์ฉํด ๋ณด์ด์ง ์๋๋ก ์ค์ ํ์๋๋ฐ, ์ด๋ฐ ๋ฐฉ์์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ์ ๋๋ฉ์ด์ ์์ด ๊ทธ๋ฅ ๋ง์ดํ์ด์ง ์ฐฝ์ด ์ฌ๋ผ์ง ์๋ฐ์ ์์๋ค. ๋ฐ๋ผ์ ๋ค์๊ณผ ๊ฐ์ด display:none ์์ฑ๊ณผ keyframes์ ์์ฑํ๊ณ animation์ ์ง์ ํ๋ ๋ฐฉ์์์ transform์ ์ง์ ์ง์ ํ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๋ค.// ๋ณ๊ฒฝ ์ import { css, keyframes } from '@emotion/react'; import styled from '@emotion/styled'; const slideLeft = keyframes` from { transform: translateX(0px); } to { transform: translateX(-600px); } `; const slideRight = keyframes` from { transform: translateX(-600px); } to { transform: translateX(0px); } `; const MypageDiv = styled.div<{ isOpen: boolean }>` position: absolute; top: 0; bottom: 0; right: 0; left: 0; width: 100%; height: 100%; background-color: white; display: block; z-index: 100; ${({ isOpen }) => { if (!isOpen) { return css` animation-name: ${slideLeft}; display: none; `; } }} overflow: hidden; animation-duration: 250ms; animation-timing-function: ease-out; animation-name: ${slideRight}; animation-fill-mode: forwards; `;
// ๋ณ๊ฒฝ ํ import { css } from '@emotion/react'; import styled from '@emotion/styled'; const MypageDiv = styled.div<{ isOpen: boolean }>` // ๋์ผํ ๋ถ๋ถ ์๋ต transform: translateX(-100%); transition: 0.7s; display: block; ${({ isOpen }) => { if (isOpen) { return css` transform: translateX(0); `; } }} `;
๋ง์ดํ์ด์ง์ ์ ๋๋ฉ์ด์ ์ด์์ ๋ง์ฐฌ๊ฐ์ง๋ก ์ํ ์์ธ ํ์ด์ง์ ์ต์ ์ ์ ํํ๋ ์ฐฝ์์ ๋ซํ ๋๋ง ์ ๋๋ฉ์ด์ ์ด ์ ์ฉ๋์ง ์๋ ๋ฌธ์ ๋ฅผ ๊ฐ์ ๋ฐฉ๋ฒ์ผ๋ก ํด๊ฒฐํ ์ ์์๋ค.
4. cypress๋ฅผ ์ฌ์ฉํ E2E ํ ์คํธ
์ด๋ฒ ํ๋ก์ ํธ๋ ์ฒ์ ์ฌ์ฉํด ๋ณด๋ ๊ธฐ์ ์ด ๋ง์ ๋งํผ ์๋ฏธ ์๋ ํ๋ก์ ํธ์ธ ๊ฒ ๊ฐ๋ค. E2E ํ ์คํธ๋ ์ฒ์์ผ๋ก ๋์ ํด ๋ณด๊ฒ ๋์๋ค. ๋ง์ง๋ง ๋ ์๋ฒฝ๊น์ง ์ ๋ ์ ์ ๊ฐ์ค๋ก ์ด๋ฌ์ฟต์ ๋ฌ์ฟต ๋ถ๋ชํ๊ณ ์์์ง๋ง E2E ํ ์คํธ๋ฅผ ๊ตฌํํด ๋ณด๋ผ๋ ์๊ตฌ์ฌํญ๋ ๋ฌด์ํ ์๋ ์์๋ค.
๊ฒ์ ๊ฒฐ๊ณผ cypress๋ฅผ ๊ฐ์ฅ ๋ง์ด ์ฌ์ฉํ๋ ๊ฒ ๊ฐ์๋ค. ๊ทธ๋์ ์ผ๋จ cypress๋ฅผ ์ฌ์ฉํ๊ธฐ๋ก ํ๋ค.
๊ทธ๋ฆฌ๊ณ ์๊ฐ์ด ์๋ค๊ณ ํ๋จํด cypress์์ ์ ๊ณตํ๋ ์ ํ์ ์ด๋ฆ์ ์ ์ํด ์ฃผ๋ ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋๋ฐ, CSS in Js ๋ฐฉ์์ผ๋ก ์คํ์ผ๋ง์ ํด์ ๊ทธ๋ฐ์ง ํด๋์ค ์ด๋ฆ์ด ๊น๋ํ์ง ์๊ณ ์๋ฒ๋ฅผ ๋ค์ ์ผค ๋๋ง๋ค ๋ฐ๋ ๊ฐ๋ฅ์ฑ์ด ์์๋ค. ๊ทธ๋์ ์ ๋๋ก ์๋ํ์ง ์์๋ค.
๋ฐ๋ผ์ cypress์์ ๊ถ์ฅํ๋
data-cy
์์ฑ์ ์ถ๊ฐํ๊ณ E2E ํ ์คํธ๋ฅผ ๋ค์ ์์ฑํ๋ค.// ๊ณ ๊ฐ์ผํฐ e2e ํ ์คํธ ์์ // cypress/integration/contact.spec.js describe('๊ณ ๊ฐ์ผํฐ', () => { beforeEach(() => { cy.visit('/'); }); it('should navigate from homepage to the contact page', () => { cy.get('[data-cy=hamburger-menu]').click(); cy.get('[data-cy=contacts]').contains('๊ณ ๊ฐ์ผํฐ').click(); cy.url().should('include', '/contacts'); cy.get('[data-cy=qna-list] > :nth-child(1)').click(); cy.get('[data-cy=qna-answer]') .contains( '๋์ฝ๋จธ๋, ๋ชจ๋ฐ์ผ ์ฟ ํฐ์ ํ๊ธ์ฑ ์ ๊ฐ์ฆ๊ถ์ ํด๋น๋๋ฏ๋ก, ํ๊ธ์์์ฆ์ด ๋ณ๋๋ก ๋ฐํ๋์ง ์์ต๋๋ค. ๋งค์ฅ์์ ๊ตฌ๋งคํ์ ์ฟ ํฐ์ผ๋ก ๊ฒฐ์ ์ ์ง์์๊ฒ ์์ฒญํ์๋ฉด ๋ฐ๊ธ์ด ๊ฐ๋ฅํฉ๋๋ค. ๋จ, ์ผ๋ถ ์ด๋ฒคํธ ์ฟ ํฐ์ ๊ฒฝ์ฐ ํ๊ธ ์์์ฆ ๋ฐํ์ด ๋ถ๊ฐํ ์ ์์ผ๋ฉฐ ํด๋น ์ฌ์ ๋ก๋ ํ๋ถ์ด ๋ถ๊ฐํ ์ ์ํด ๋ถํ๋๋ฆฝ๋๋ค.', ) .should('be.visible'); cy.get('[data-cy=contact-tap-2]').click(); cy.get('[data-cy=qna-list] > :nth-child(1)').contains( '๋ฐ๋ก ์ ์ฐ์ ์ ๋๋์?', ); cy.get('[data-cy=qna-list] > :nth-child(1)').click(); cy.get('[data-cy=qna-answer]') .contains( 'ํ๋งคํ์ ์ฟ ํฐ์ ์ฌ๊ณ ๋ฐ์์จ์ ์ค์ด๊ธฐ ์ํด ์๋ฝ์ผ์ ๊ธฐ์ค์ผ๋ก 2์์ ์ผ ํ์ ์ ์ฐ๊ธ์ผ๋ก ์ ํ๋๋ฉฐ, ์ํ ๊ฑฐ๋ ์์คํ ์ ๋ฐ๋ก ์ง๊ธ์ ์ด๋ ค์ด ์ ์ํด ๋ถํ๋๋ฆฌ๊ฒ ์ต๋๋ค.', ) .should('be.visible'); cy.get('[data-cy=back-menu]').click(); cy.wait(500); cy.get('[data-cy="header-title"]').contains('๋์ฝ๋ด์ฝ'); }); });
์ฒซ E2E ํ ์คํธ๋ ๊น๋ํ๊ฒ ์์ฑํ๊ธฐ ์ฑ๊ณต!
์๋ง ํ๋ฆฌ์จ๋ณด๋ฉ ๊ต์ก ์ค ์ ์ผ ์ค๋ ๊ฑธ๋ฆฌ๊ณ ์ด๋ ค์ ๋ ๊ณผ์ ๊ฐ ์๋๊น ์ถ๋ค. ๊ทธ๋ ์ง๋ง ํฐ ์ฐ์ ๋์ผ๋ฉด ๋ค์ ๊ณ ๋น(?)๋ ๊ทธ๋๋ง ๋๊ธฐ ์ฌ์์ง๋ค๊ณ ์ญ์ ์ด๋ ค์ด ๊ณผ์ ๋ก ๋ด๊ฐ ๋ ๋ง์ด ์ฑ์ฅํ ์ ์๋ ๊ฒ ๊ฐ๋ค. Next.js์ ๋ํด์๋ ๋ ๋ง์ด ์ฌ์ฉํด ๋ณด๊ณ , ๋ ๋ง์ด ๊ณต๋ถํด ๋ด์ผ ์ต์ํด์ง๊ฒ ์ง๋ง ์ฒซ Next.js๋ฅผ ํ์๋ค๊ณผ ํจ๊ปํด ์ฝ๊ฒ ํฌ๊ธฐํ์ง ์์ ์ ์์๊ณ , ๋ ๋น ๋ฅด๊ฒ ๋ฐฐ์ฐ์ง ์์ ์ ์์ง ์์์๊น ์๊ฐํด ๋ณธ๋ค. ์์ง ์ด๊ฒ์ ๊ฒ ๋ฐฐ์ธ ๊ฒ์ด ๋๋ฌด ๋ง์ง๋ง ํ๋ก์ ํธ๋ฅผ ํ๋ฉฐ ์์ฝ๋ค๊ณ ์๊ฐํ๋ ํฌ์ธํธ์ธ getServerSideProps์ E2E ํ ์คํธ๋ฅผ ์ฌ์ฉํด ๋ฆฌํฉํ ๋งํ๊ธฐ ๋๋ฌธ์ ๊ทธ๋๋ ๋๋ฆ ๋ฟ๋ฏํจ์ด ๋ด๊ฒจ ์๋ ํ๊ณ ๊ธ์ด๋ค. โ๐ป
์ฐธ๊ณ
Next.js + redux toolkit ๊ธฐ๋ณธ ์ธํ
Next.js์์ redux-toolkit ์ธํ ํ๊ธฐ
Cypress Elements ์ ํํ๊ธฐ (data-cy ์ฌ์ฉ๊ณผ ์ ๊ฑฐ)
ํ ๋ ํฌ์งํ ๋ฆฌ
๊ฐ์ธ ๋ ํฌ์งํ ๋ฆฌ (๋ฆฌํฉํ ๋ง ์ฝ๋ ํฌํจ)
'ETC' ์นดํ ๊ณ ๋ฆฌ์ ๋ค๋ฅธ ๊ธ