ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • ์›ํ‹ฐ๋“œ ํ”„๋ฆฌ์˜จ๋ณด๋”ฉ ํ”„๋ก ํŠธ์—”๋“œ ์ฝ”์Šค 3์ฃผ์ฐจ ํšŒ๊ณ  (1) — Next.js
    ETC 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

     

    ๋Œ“๊ธ€

Designed by Tistory.