ETC

์›ํ‹ฐ๋“œ ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ ํ”„๋ก ํŠธ์—”๋“œ ์ฝ”์Šค 3์ฃผ์ฐจ ํšŒ๊ณ  (1) — Next.js

ttaerrim 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๋กœ ๋ฐ”๊พธ๋„๋ก ์ˆ˜์ •ํ•ด ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.

 

3,620์›์ด ์•„๋‹Œ 3620์›์œผ๋กœ ํ‘œ์‹œ

 

 

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 ์„ธํŒ…ํ•˜๊ธฐ

E2Eํ…Œ์ŠคํŠธ with Cypress

Cypress Elements ์„ ํƒํ•˜๊ธฐ (data-cy ์‚ฌ์šฉ๊ณผ ์ œ๊ฑฐ)

 

 

 

ํŒ€ ๋ ˆํฌ์ง€ํ† ๋ฆฌ

 

GitHub - console-lo9/ncnc-app: ๋‹ˆ์ฝ˜๋‚ด์ฝ˜ ๋ชจ๋ฐ”์ผ ์›นํŽ˜์ด์ง€๋ฅผ ํด๋ก ํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

๋‹ˆ์ฝ˜๋‚ด์ฝ˜ ๋ชจ๋ฐ”์ผ ์›นํŽ˜์ด์ง€๋ฅผ ํด๋ก ํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. Contribute to console-lo9/ncnc-app development by creating an account on GitHub.

github.com

 

 

๊ฐœ์ธ ๋ ˆํฌ์ง€ํ† ๋ฆฌ (๋ฆฌํŒฉํ† ๋ง ์ฝ”๋“œ ํฌํ•จ)

 

GitHub - ttaerrim/ncnc-app: ๋‹ˆ์ฝ˜๋‚ด์ฝ˜ ๋ชจ๋ฐ”์ผ ์›นํŽ˜์ด์ง€๋ฅผ ํด๋ก ํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.

๋‹ˆ์ฝ˜๋‚ด์ฝ˜ ๋ชจ๋ฐ”์ผ ์›นํŽ˜์ด์ง€๋ฅผ ํด๋ก ํ•œ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. Contribute to ttaerrim/ncnc-app development by creating an account on GitHub.

github.com