KDT/TIL

10/12 TIL : React SPA ์ œ์ž‘(Mbti-app)

ebulsok 2022. 10. 17. 13:42

๐Ÿ”Ž ๋ฆฌ์•กํŠธ SPA(Single Page Application) ์ œ์ž‘

  • [ํ„ฐ๋ฏธ๋„] npx create-react-app mbti-app
  • [ํ„ฐ๋ฏธ๋„] npm i redux react-redux @reduxjs/toolkit styled-components prettier
  • .prettierrc
{
    "semi": true,
    "singleQuote": true
}
  • /.vscode/settings.json
{
    "[javascript]": {
        "editor.maxTokenizationLineLength": 2500,
        "editor.formatOnSave": true,
        "editor.defaultFormatter": "esbenp.prettier-vscode"
    }
}
  • ํด๋” ๊ตฌ์กฐ ์„ธํŒ…

 

๐Ÿšฉ redux ๊ธฐ์ดˆ ์„ธํŒ…

// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import rootReducer from './store/index'; // index ์ƒ๋žตํ•ด๋„ ๋™์ผ

const reduxDevTool =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

const store = configureStore({ reducer: rootReducer }, reduxDevTool);
console.log(store.getState());

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

reportWebVitals();

 

๐Ÿšฉ rootReducer ์„ค์ •

// src/store/index.js
 import { combineReducers } from 'redux';
import mbti from './modules/mbti';

// reducer ๋“ค์„ combine ํ•ด์„œ export
export default combineReducers({
  mbti,
});

 

๐Ÿšฉ mbti store ์„ค์ •

  • ์ดˆ๊ธฐ state๋ฅผ ์„ค์ •: mbti ์งˆ๋ฌธ ๋ชฉ๋ก, ํ˜„์žฌ ํŽ˜์ด์ง€ ๊ฐ’, mbti ์ „์ฒด ๊ฒฐ๊ณผ ๊ฐ’, ์ „์ฒด ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ์„ค๋ช… ๊ฐ’, ์ถ”๊ฐ€ ์ด๋ฏธ์ง€ ์ฃผ์†Œ ๊ฐ’
  • ์•„์ง DB ์—ฐ๋™์„ ํ•˜์ง€ ์•Š์„ ๊ฒƒ์ด๋ฏ€๋กœ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ ์„ค์ •
  • ์•ก์…˜ ํƒ€์ž… ์„ค์ •
  • ์•ก์…˜ ํ•จ์ˆ˜ ์„ค์ •
  • ์‹ค์ œ๋กœ state ๊ฐ’์„ ๋ณ€๊ฒฝ์‹œ์ผœ์ค„ reducer ๋งŒ๋“ค๊ธฐ
// src/store/modules/mbti.js
// ์ดˆ๊ธฐ ์ƒํƒœ ์„ค์ •
const initState = {
  mbtiResult: '',
  page: 0, // 0: ์ธํŠธ๋กœ ํŽ˜์ด์ง€, 1 ~ n: ์„ ํƒ ํŽ˜์ด์ง€, n+1: ๊ฒฐ๊ณผ ํŽ˜์ด์ง€
  survey: [
    {
      question:
        'ํ‡ด๊ทผ ์ง์ „์— ๋™๋ฃŒ๋กœ๋ถ€ํ„ฐ ๊ฐœ๋ฐœ์ž ๋ชจ์ž„์— ์ดˆ๋Œ€๋ฅผ ๋ฐ›์€ ๋‚˜!!\n\nํ‡ด๊ทผ ์‹œ๊ฐ„์— ๋‚˜๋Š”?',
      answer: [
        {
          text: '๊ทธ๋Ÿฐ ๋ชจ์ž„์„ ์™œ ์ด์ œ์„œ์•ผ ์•Œ๋ ค ์ค€๊ฑฐ์•ผ! ๋‹น์žฅ ๋ชจ์ž„์œผ๋กœ ์ถœ๋ฐœํ•œ๋‹ค',
          result: 'E',
        },
        {
          text: '1๋…„ ์ „์— ์•Œ๋ ค์คฌ์–ด๋„ ์•ˆ๊ฐ”์„ ๊ฑด๋ฐ ๋ญ”... ๋” ๋น ๋ฅด๊ฒŒ ์ง‘์œผ๋กœ ๊ฐ„๋‹ค',
          result: 'I',
        },
      ],
    },
    {
      question:
        '์ƒˆ๋กœ์šด ์„œ๋น„์Šค ๊ฐœ๋ฐœ ์ค‘์—, ๋™๋ฃŒ๊ฐ€ ์ƒˆ๋กœ ๋‚˜์˜จ ์‹ ๊ธฐ์ˆ ์„ ์“ฐ๋Š”๊ฒŒ ๋” ํŽธํ• ๊ฑฐ๋ผ๊ณ  ์ถ”์ฒœ์„ ํ•ด์ค€๋‹ค!\n\n๋‚˜์˜ ์„ ํƒ์€!?',
      answer: [
        {
          text: '๋ญ”์†Œ๋ฆฌ์—ฌ, ๊ทธ๋ƒฅ ํ•˜๋˜ ๋Œ€๋กœ ๊ฐœ๋ฐœํ•˜๋ฉด ๋˜๋Š”๊ฑฐ์ง€! ๊ธฐ์กด ์ƒ๊ฐ๋Œ€๋กœ ๊ฐœ๋ฐœํ•œ๋‹ค',
          result: 'S',
        },
        {
          text: '์˜คํ˜ธ? ๊ทธ๋Ÿฐ๊ฒŒ ์žˆ์–ด? ์ผ๋‹จ ๊ตฌ๊ธ€์„ ์ฐพ์•„๋ณธ๋‹ค',
          result: 'N',
        },
      ],
    },
    {
      question:
        '์„œ๋น„์Šค ์ถœ์‹œ ์ดํ‹€ ์ „ ์•ผ๊ทผ ์‹œ๊ฐ„, ๊ฐ‘์ž๊ธฐ ๋™๋ฃŒ๊ฐ€ ์–ด!?๋ฅผ ์™ธ์ณค๋‹ค!\n\n๋‚˜์˜ ์„ ํƒ์€?',
      answer: [
        {
          text: '๋ฌด์Šจ ๋ฒ„๊ทธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฑฐ์ง€? ์•„๋งˆ DB ๊ด€๋ จ ๋ฒ„๊ทธ๊ฐ€ ์•„๋‹๊นŒ? ๋น ๋ฅด๊ฒŒ ๋™๋ฃŒ์˜ ์ž๋ฆฌ๋กœ ๋‹ฌ๋ ค๊ฐ„๋‹ค',
          result: 'T',
        },
        {
          text: '์•„... ๋‚ด์ผ๋„ ์•ผ๊ทผ ๊ฐ์ด๊ตฌ๋‚˜ ใ… ใ… ! ์ผ๋‹จ ๋™๋ฃŒ์˜ ์ž๋ฆฌ๋กœ ๊ฐ€ ๋ณธ๋‹ค',
          result: 'F',
        },
      ],
    },
    {
      question:
        'ํŒ€์žฅ๋‹˜์ด xx ์”จ ๊ทธ์ „์— ๋งํ•œ ๊ธฐ๋Šฅ ๋‚ด์ผ ์˜คํ›„๊นŒ์ง€ ์™„๋ฃŒ ๋ถ€ํƒํ•ด์š”๋ผ๊ณ  ๋งํ–ˆ๋‹ค!\n\n๋‚˜์˜ ์„ ํƒ์€?',
      answer: [
        {
          text: '์ผ๋‹จ ๋น ๋ฅด๊ฒŒ ๊ฐœ๋ฐœ ์™„๋ฃŒํ•˜๊ณ , ๋‚˜๋จธ์ง€ ์‹œ๊ฐ„์— ๋…ผ๋‹ค',
          result: 'J',
        },
        {
          text: '๊ทธ๊ฑฐ ๋‚ด์ผ ์•„์นจ์— ์™€์„œ ๊ฐœ๋ฐœํ•ด๋„ ์ถฉ๋ถ„ ํ•˜๊ฒ ๋Š”๋ฐ? ์ผ๋‹จ ๋…ผ๋‹ค',
          result: 'P',
        },
      ],
    },
  ],

  explaination: {
    ESTJ: {
      text: '๋ฌด๋ฆฌํ•œ ๊ฐœ๋ฐœ ์ผ์ •๋งŒ ์•„๋‹ˆ๋ผ๋ฉด ์ผ์ •์„ ์ฒ ์ €ํ•˜๊ฒŒ ์ง€ํ‚ฌ ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/estj.jpg',
    },
    ISTJ: {
      text: '์Šค์Šค๋กœ ํ•˜๊ณ ์‹ถ์€ ๋ถ„์•ผ๋ฅผ ๋๊นŒ์ง€ ํŒŒ๊ณ  ๋“ค์–ด์„œ ๋๋‚ด ์„ฑ๊ณต ์‹œํ‚ฌ ๋‹น์‹ ์˜ MBTI ๋Š”!',
      img: '/images/istj.jpg',
    },
    ENTJ: {
      text: '๋ฏธ๋ž˜์˜ ๋Šฅ๋ ฅ ์ฉŒ๋Š” ๊ฐœ๋ฐœ ํŒ€์žฅ๋‹˜์œผ๋กœ ๊ฐœ๋ฐœํŒ€์„ ์ด๋Œ ๋‹น์‹ ์˜ MBTI ๋Š”!',
      img: '/images/entj.jpg',
    },
    INTJ: {
      text: 'ํ˜ผ์ž์„œ ๋ชจ๋“  ๊ฒƒ์„ ๋‹ค ํ•ด๋‚ด๋Š” ์›๋งจ ์บ๋ฆฌ์˜ ํ‘œ๋ณธ! ๋‹น์‹ ์˜ MBTI ๋Š”!',
      img: '/images/intj.jpg',
    },
    ESFJ: {
      text: '๊ฐœ๋ฐœํŒ€์˜ ๋ถ„์œ„๊ธฐ ๋ฉ”์ด์ปค์ด์ž ์•„์ด๋””์–ด ๋ฑ…ํฌ๊ฐ€ ๋  ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/esfj.jpg',
    },
    ISFJ: {
      text: '๊ฐœ๋ฐœํŒ€์˜ ๋งˆ๋” ํ…Œ๋ ˆ์‚ฌ, ๊ณ ๋ฏผ ์ƒ๋‹ด์†Œ ์—ญํ• ์„ ์ž์ฒ˜ํ•˜๋Š” ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/isfj.jpg',
    },
    ENFJ: {
      text: '๋‹น์‹ ์ด ์žˆ๋Š” ํŒ€์€ ์–ธ์ œ๋‚˜ ์˜ฌ๋ฐ”๋ฅธ ๊ณณ์„ ํ–ฅํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค! ํŒ€์›์€ ๋ฌผ๋ก  ํŒ€์˜ ๋ฐฉํ–ฅ์„ ์ฑ™๊ธฐ๋Š” ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/enfj.jpg',
    },
    INFJ: {
      text: '์˜ˆ๋ฆฌํ•œ ํ†ต์ฐฐ๋ ฅ์œผ๋กœ ๋ชจ๋“  ๊ฒƒ์„ ๋‚ด๋‹ค๋ณด๋ฉด์„œ ์™„๋ฒฝํ•˜๊ฒŒ ๊ฐœ๋ฐœ์„ ํ•  ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/infj.jpg',
    },
    ESTP: {
      text: '์ฟจํ•˜๊ฒŒ ์ž์‹ ์ด ํ•  ๊ฒƒ์„ ํ•˜๋ฉด์„œ ๋…ผ๋ฆฌ์ ์ธ ๊ฐœ๋ฐœ์„ ํ•  ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/estp.jpg',
    },
    ISTP: {
      text: '๋‹จ์‹œ๊ฐ„์—๋„ ํšจ์œจ์ ์œผ๋กœ ๊ฐœ๋ฐœํ•˜์—ฌ ๋ชจ๋“  ๊ฒƒ์„ ์™„์„ฑํ•  ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/istp.jpg',
    },
    ENTP: {
      text: '์Šค์Šค๋กœ ํฅ๋ฏธ๋งŒ ์ƒ๊ธด๋‹ค๋ฉด ๋‹น์žฅ์— ํŽ˜์ด์Šค๋ถ๋„ ๋งŒ๋“ค์–ด ๋ฒ„๋ฆด ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/entp.jpg',
    },
    INTP: {
      text: 'ํ™•์‹คํ•œ ์ฃผ๊ด€๊ณผ ๋›ฐ์–ด๋‚œ ์ง€๋Šฅ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋…ผ๋ฆฌ์  ๊ฐœ๋ฐœ์„ ํ•  ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/intp.jpg',
    },
    ESFP: {
      text: '๊ฐœ๋ฐœํŒ€์˜ ์—๋„ˆ์ž์ด์ €! ๊ฐœ๋ฐœํŒ€ ํŠน์œ ์˜ ์„œ๋จนํ•จ์„ ๊นจ๋Š” ๋‹น์‹ ! ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/esfp.jpg',
    },
    ISFP: {
      text: '๋›ฐ์–ด๋‚œ ํ˜ธ๊ธฐ์‹ฌ๊ณผ ์˜ˆ์ˆ ์  ๊ฐ๊ฐ์œผ๋กœ ๊ฐœ๋ฐœํŒ€์˜ ๋ถ€์กฑํ•จ์„ ์ฑ„์›Œ๊ฐˆ ๋‹น์‹ ! ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/isfp.jpg',
    },
    ENFP: {
      text: '์ž์œ ๋กœ์šด ์˜ํ˜ผ์œผ๋กœ ๊ฐœ๋ฐœํŒ€์˜ ์œคํ™œ์œ  ๋ฐ ํ™œ๋ ฅ์†Œ๊ฐ€ ๋˜์–ด์ค„ ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/enfp.jpg',
    },
    INFP: {
      text: '๊ฐœ๋ฐœํŒ€์˜ ๊ทธ ์–ด๋–ค ํŠธ๋Ÿฌ๋ธ”๋„ ๋‹น์‹  ์•ž์—์„œ๋Š” ์‚ฌ๋ฅด๋ฅด ๋…น์„๋ฟ, ํŒ€์˜ ๊ทผ๊ฐ„์„ ๋‹ค์ ธ์ฃผ๋Š” ๋‹น์‹ ์˜ MBTI๋Š”!',
      img: '/images/infp.jpg',
    },
  },
};

// ์•ก์…˜ ํƒ€์ž…(๋ฌธ์ž์—ด)
const CHECK = 'mbti/CHECK';
const NEXT = 'mbti/NEXT';
const RESET = 'mbti/RESET';

// ์•ก์…˜ ์ƒ์„ฑ ํ•จ์ˆ˜
export function next() {
  return {
    type: NEXT,
  };
}

export function check(result) {
  return {
    type: CHECK,
    payload: { result },
  };
}

export function reset() {
  return {
    type: RESET,
  };
}

// reducer
export default function mbti(state = initState, action) {
  switch (action.type) {
    case CHECK:
      return {
        ...state,
        mbtiResult: state.mbtiResult + action.payload.result,
      };
    case NEXT:
      return {
        ...state,
        page: state.page + 1,
      };
    case RESET:
      return {
        ...state,
        page: 0,
        mbtiResult: '',
      };
    default:
      return state;
  }
}

* ๋‚ด์šฉ์€ ์ˆ˜์—…๊ณผ ๋˜‘๊ฐ™์ด ์ž‘์„ฑํ•œ ๊ฒƒ์œผ๋กœ, mbti์— ๋Œ€ํ•œ ์ •ํ™•ํ•œ ์ง€์‹์„ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•œ ๊ฒƒ์ด ์•„๋‹™๋‹ˆ๋‹ค!

 

๐Ÿšฉ ์‹œ์ž‘ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ ์ œ์ž‘

// src/components/Start.js
import styled from 'styled-components';
import OrangeButton from './OrangeButton';

export default function Start() {
  return (
    <>
      <Header>๊ฐœ๋ฐœ์ž MBTI ์กฐ์‚ฌ</Header>
      <MainImg src="/images/main.jpg" alt="๋ฉ”์ธ ์ด๋ฏธ์ง€" />
      <SubHeader>
        ๊ฐœ๋ฐœ์ž๊ฐ€ ํ”ํžˆ ์ ‘ํ•˜๋Š” ์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ MBTI๋ฅผ ์•Œ์•„๋ด…์‹œ๋‹ค!
      </SubHeader>
      <OrangeButton text="ํ…Œ์ŠคํŠธ ์‹œ์ž‘" />
    </>
  );
}

const MainImg = styled.img`
  width: inherit;
`;

const Header = styled.p`
  font-size: 3em;
`;

const SubHeader = styled.p`
  font-size: 1.5em;
  color: #777;
`;
// src/App.js
import styled from 'styled-components';
import Start from './components/Start';

const Main = styled.main`
  box-sizing: border-box;
  width: 100%;
  max-width: 500px;
  padding: 0 35px;
  margin: auto;
  text-align: center;
`;

function App() {
  return (
    <Main>
      <Start />
    </Main>
  );
}

export default App;
// src/components/Button.js
import styled from 'styled-components';

export default function Button({
  text,
  clickEvent,
  mainColor,
  subColor,
  hoverColor,
}) {
  return (
    <MyButton
      onClick={clickEvent}
      mainColor={mainColor}
      subColor={subColor}
      hoverColor={hoverColor}
    >
      {text}
    </MyButton>
  );
}

const MyButton = styled.a`
  position: relative;
  display: inline-block;
  cursor: pointer;
  vertical-align: middle;
  text-decoration: none;
  line-height: 1.6em;
  font-size: 1.2em;
  padding: 1.25em 2em;
  background-color: ${(props) => props.mainColor};
  border: 2px solid ${(props) => props.subColor};
  border-radius: 0.75em;
  user-select: none; // ๋“œ๋ž˜๊ทธ ๋ฐฉ์ง€
  transition: transform 0.15s ease-out;
  transform-style: preserve-3d;
  margin-top: 1em;
  
  // ๊ทธ๋ฆผ์ž
  &::before {
    content: '';
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    right: 0;
    left: 0;
    right: 0;
    background: ${(props) => props.subColor};
    border-radius: inherit;
    box-shadow: 0 0 0 2px ${(props) => props.subColor};
    transform: translate3d(0, 0.75em, -1em);
  }
  &:hover {
    background: ${(props) => props.hoverColor};
    transform: translateY(0.25em);
  }
`;

 

๐Ÿšฉ OrangeButton์œผ๋กœ ํŠน์ˆ˜ํ™”

// src/components/OrangeButton.js
import Button from './Button';

export default function OrangeButton({ text, clickEvent }) {
  return (
    <Button
      text={text}
      clickEvent={clickEvent}
      mainColor="#fae243"
      subColor="#fa9f1a"
      hoverColor="#faf000"
    />
  );
}

 

๐Ÿšฉ OrangeButton ์ ์šฉ

// src/components/Start.js
    return (
        <>
          <Header>๊ฐœ๋ฐœ์ž MBTI ์กฐ์‚ฌ</Header>
          <MainImg src="/images/main.jpg" alt="๋ฉ”์ธ ์ด๋ฏธ์ง€" />
          <SubHeader>
            ๊ฐœ๋ฐœ์ž๊ฐ€ ํ”ํžˆ ์ ‘ํ•˜๋Š” ์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ MBTI๋ฅผ ์•Œ์•„๋ด…์‹œ๋‹ค! ์ง€๊ธˆ๊นŒ์ง€{'\n\n'}
            {counts} ๋ช…์ด ์ฐธ์—ฌํ•ด์ฃผ์…จ์Šต๋‹ˆ๋‹ค.
          </SubHeader>
          <OrangeButton text="ํ…Œ์ŠคํŠธ ์‹œ์ž‘" clickEvent={() => dispatch(next())} />
        </>
      );

 

๐Ÿšฉ styled-components GlobalStyle

  • ํŽ˜์ด์ง€ ์ „์ฒด์— ๋Œ€ํ•œ ์Šคํƒ€์ผ์„ ์ ์šฉํ•  ๋•Œ ์‚ฌ์šฉ
// src/components/GlobalStyle.js
import { createGlobalStyle } from 'styled-components';

const GlobalStyle = createGlobalStyle`
    @font-face {
    font-family: 'HS-Regular';
    src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/noonfonts_2201-2@1.0/HS-Regular.woff') format('woff');
    font-weight: normal;
    font-style: normal;
}

    body {
        font-family: 'HS-Regular', "Arial", sans-serif;
        padding-top: 1em;
        white-space: pre-wrap;
    }

    ul, ol {
        list-style: none;
        padding-left: 0px;
    }
`;

export default GlobalStyle;
// src/app.js
    return (
        <>
          <GlobalStyle />
          <Main>
              <Start />
          </Main>
        </>
      );

 

๐Ÿšฉ ํŽ˜์ด์ง€ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ

  • page๊ฐ€ 0์ด๋ฉด Start ์ปดํฌ๋„ŒํŠธ ๋ณด์—ฌ์ฃผ๊ธฐ
  • page๊ฐ€ ์„ค๋ฌธ์˜ ๊ธธ์ด์™€ ๊ฐ™์„ ๋•Œ๊นŒ์ง€ ์„ค๋ฌธ์กฐ์‚ฌ ์ปดํฌ๋„ŒํŠธ ๋ณด์—ฌ์ฃผ๊ธฐ
  • page๊ฐ€ ์„ค๋ฌธ์˜ ๊ธธ์ด๋ฅผ ๋„˜์–ด๊ฐ€๋ฉด ๊ฒฐ๊ณผ ์ปดํฌ๋„ŒํŠธ ๋ณด์—ฌ์ฃผ๊ธฐ
// src/App.js
    return (
        <>
          <GlobalStyle />
          <Main>
            {page === 0 ? (
              <Start />
            ) : page !== survey.length + 1 ? (
              <Mbti />
            ) : (
              <Show />
            )}
          </Main>
        </>
      );

 

๐Ÿšฉ ์„ค๋ฌธ ์ปดํฌ๋„ŒํŠธ ์ œ์ž‘, SkyblueButton ํŠน์ˆ˜ํ™”

// src/components/Mbti.js
import { useSelector } from 'react-redux';
import styled from 'styled-components';
import SkyBlueButton from './SkyblueButton';

const SurveyQuestion = styled.p`
  font-size: 1.5em;
  color: #777;
`;

const Vs = styled.p`
  font-size: 2em;
  padding-top: 1em;
`;

export default function Mbti() {
  const survey = useSelector((state) => state.mbti.survey);
  const page = useSelector((state) => state.mbti.page);

  return (
    <>
      <SurveyQuestion>{survey[page - 1].question}</SurveyQuestion>
      <ul>
        {survey[page - 1].answer.map((el, index) => {
          return (
            <li key={index}>
              <SkyBlueButton
                text={el.text}
              />
              {index === 0 && <Vs>VS</Vs>}
            </li>
          );
        })}
      </ul>
    </>
  );
}
// src/components/SkyblueButton.js
import Button from './Button';

export default function SkyBlueButton({ text, clickEvent }) {
  return (
    <Button
      text={text}
      clickEvent={clickEvent}
      mainColor="#7EDCFA"
      subColor="#3A82E0"
      hoverColor="#CFECF2"
    />
  );
}

 

๐Ÿšฉ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ์— ์•ก์…˜ ์ƒ์„ฑ ํ•จ์ˆ˜ ์ง€์ •

  • ํŽ˜์ด์ง€๋ฅผ ๋„˜๊ธฐ๋Š” ๊ธฐ๋Šฅ์„ ํ•˜๋Š” next()๋ฅผ dispatch๋ฅผ ์ด์šฉํ•˜์—ฌ Reducer์— ์ „๋‹ฌ
  • Start ์ปดํฌ๋„ŒํŠธ์˜ ํ…Œ์ŠคํŠธ ์‹œ์ž‘์ด๋ผ๋Š” ๋ฒ„ํŠผ, Mbti ์ปดํฌ๋„ŒํŠธ์˜ ์„ ํƒ ๋ฒ„ํŠผ์— ์ง€์ •
// src/components/Start.js
import { useDispatch } from 'react-redux';
import styled from 'styled-components';
import OrangeButton from './OrangeButton';
import { next } from '../store/modules/mbti';

export default function Start() {;
  const dispatch = useDispatch();

  return (
    <>
      <Header>๊ฐœ๋ฐœ์ž MBTI ์กฐ์‚ฌ</Header>
      <MainImg src="/images/main.jpg" alt="๋ฉ”์ธ ์ด๋ฏธ์ง€" />
      <SubHeader>
        ๊ฐœ๋ฐœ์ž๊ฐ€ ํ”ํžˆ ์ ‘ํ•˜๋Š” ์ƒํ™ฉ์— ๋”ฐ๋ผ์„œ MBTI๋ฅผ ์•Œ์•„๋ด…์‹œ๋‹ค! ์ง€๊ธˆ๊นŒ์ง€{'\n\n'}
        {counts} ๋ช…์ด ์ฐธ์—ฌํ•ด์ฃผ์…จ์Šต๋‹ˆ๋‹ค.
      </SubHeader>
      <OrangeButton text="ํ…Œ์ŠคํŠธ ์‹œ์ž‘" clickEvent={() => dispatch(next())} />
    </>
  );
}

const MainImg = styled.img`
  width: inherit;
`;

const Header = styled.p`
  font-size: 3em;
`;

const SubHeader = styled.p`
  font-size: 1.5em;
  color: #777;
`;
// src/components/Mbti.js
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import SkyBlueButton from './SkyblueButton';
import { next } from '../store/modules/mbti';

const SurveyQuestion = styled.p`
  font-size: 1.5em;
  color: #777;
`;

const Vs = styled.p`
  font-size: 2em;
  padding-top: 1em;
`;

export default function Mbti() {
  const survey = useSelector((state) => state.mbti.survey);
  const page = useSelector((state) => state.mbti.page);
  const dispatch = useDispatch();

  return (
    <>
      <SurveyQuestion>{survey[page - 1].question}</SurveyQuestion>
      <ul>
        {survey[page - 1].answer.map((el, index) => {
          return (
            <li key={index}>
              <SkyBlueButton
                text={el.text}
                clickEvent={() => {
                  dispatch(next());
                }}
              />
              {index === 0 && <Vs>VS</Vs>}
            </li>
          );
        })}
      </ul>
    </>
  );
}

 

๐Ÿšฉ Progress Bar ๋งŒ๋“ค๊ธฐ, ์‚ฝ์ž…

  • ๊ฐ’์€ ํ˜„์žฌ page / ์ „์ฒด ์„ค๋ฌธ ๋ฐฐ์—ด์˜ ๊ธธ์ด
  • Progress Bar์˜ ๋ฐ”๊นฅ ๋ถ€๋ถ„์„ ๋จผ์ € ๊ทธ๋ฆฌ๊ณ , ์ž์‹ ์š”์†Œ๊ฐ€ ๋ถ€๋ชจ์˜ ํฌ๊ธฐ๋ฅผ ์ƒ์†ํ•œ ๋‹ค์Œ ์ƒ‰์„ ์ž…ํ˜€์„œ %๋กœ ๊ตฌํ˜„
// src/components/Progress.js
import styled from 'styled-components';

export default function Progress({ page, maxPage }) {
  return (
    <MyProgress>
      <div>
        {page} / {maxPage}
      </div>
      <Fill>
        <Gauge percent={(page / maxPage) * 100}></Gauge>
      </Fill>
    </MyProgress>
  );
}

const MyProgress = styled.div`
  margin-top: 3em;
`;

const Fill = styled.div`
  width: 100%;
  height: 10px;
  background-color: #777;
  margin-top: 1em;
  text-align: left;
`;

const Gauge = styled.div`
  background-color: skyblue;
  display: inline-block;
  height: inherit;
  position: relative;
  top: -4px;
  width: ${(props) => props.percent}%;
`;
// src/components/Mbti.js
    return (
        <>
          <SurveyQuestion>{survey[page - 1].question}</SurveyQuestion>
          <ul>
            {survey[page - 1].answer.map((el, index) => {
              return (
                <li key={index}>
                  <SkyBlueButton
                    text={el.text}
                    clickEvent={() => {
                      dispatch(check(el.result));
                    }}
                  />
                  {index === 0 && <Vs>VS</Vs>}
                </li>
              );
            })}
          </ul>
          <Progress page={page} maxPage={survey.length} />
        </>
    );

 

๐Ÿšฉ ๊ฒฐ๊ณผ๋ฅผ ๋งŒ๋“œ๋Š” check() ์•ก์…˜ ์ƒ์„ฑ ํ•จ์ˆ˜ ์‚ฝ์ž…

  • check()๋Š” ์ „๋‹ฌ ๋ฐ›์€ ๊ฒฐ๊ณผ๋ฅผ mbtiResult๋ผ๋Š” ๋ฌธ์ž์—ด์— ์ถ”๊ฐ€ํ•ด์ฃผ๊ธฐ ๋•Œ๋ฌธ์— ํ•จ์ˆ˜๋ฅผ ํ˜ธ์ถœํ•  ๋•Œ ์„ค๋ฌธ ๊ฐ์ฒด์— ํฌํ•จ๋œ ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด๋งŒ ์ „๋‹ฌํ•˜๋ฉด ๋จ
// src/components/Mbti.js
    return (
        <>
          <SurveyQuestion>{survey[page - 1].question}</SurveyQuestion>
          <ul>
            {survey[page - 1].answer.map((el, index) => {
              return (
                <li key={index}>
                  <SkyBlueButton
                    text={el.text}
                    clickEvent={() => {
                      dispatch(check(el.result));
                      dispatch(next());
                    }}
                  />
                  {index === 0 && <Vs>VS</Vs>}
                </li>
              );
            })}
          </ul>
          <Progress page={page} maxPage={survey.length} />
        </>
     );

 

๐Ÿšฉ redux์— ๋ชจ์ธ ๊ฒฐ๊ณผ ์ถœ๋ ฅ, reset() ์•ก์…˜ ์ƒ์„ฑ ํ•จ์ˆ˜ ์ „๋‹ฌ

  • redux์—์„œ mbtiResult ๊ฐ’๊ณผ, ๊ฒฐ๊ณผ ๊ฐ’์— ๋งž๋Š” ์„ค๋ช… + ์ด๋ฏธ์ง€๋ฅผ ์ถœ๋ ฅ
  • ๋‹ค์‹œํ•˜๊ธฐ ๋ฒ„ํŠผ์— dispatch๋ฅผ ํ†ตํ•ด ํ•จ์ˆ˜ ์ „๋‹ฌ
// src/components/Show.js
import { useDispatch, useSelector } from 'react-redux';
import styled from 'styled-components';
import { reset } from '../store/modules/mbti';
import OrangeButton from './OrangeButton';

export default function Show() {
  const result = useSelector((state) => state.mbti.mbtiResult);
  const explanation = useSelector((state) => state.mbti.explanation[result]);
  const dispatch = useDispatch();

  return (
    <>
      <Header>๋‹น์‹ ์˜ ๊ฐœ๋ฐœ์ž MBTI ๊ฒฐ๊ณผ๋Š”?</Header>
      <Explanation>{explanation.text}</Explanation>
      <Result>{result}</Result>
      <Additional>์ด๊ฑด ์žฌ๋ฏธ๋กœ ์ฝ์–ด ๋ณด์„ธ์š”!</Additional>
      <AdditionalImg src={explanation.img} alt="ํŒฉํญ" />
      <OrangeButton text="๋‹ค์‹œ ๊ฒ€์‚ฌํ•˜๊ธฐ" clickEvent={() => dispatch(reset())} />
    </>
  );
}

const Header = styled.p`
  font-size: 3em;
`;

const Explanation = styled.p`
  font-size: 1.5em;
  color: #777;
`;

const Result = styled.p`
  font-size: 3em;
  color: dodgerblue;
`;

const Additional = styled.p`
  font-size: 2em;
  color: orange;
`;

const AdditionalImg = styled.img`
  width: 500px;
  transform: translateX(-35px);
`;