본문 바로가기
side project

[React][side project] mbti궁합 테스트(1)

by codnjs779 2021. 11. 25.

나와 상대의 별자리, mbti, 12지신, 혈액형을 입력하면 두사람의 궁합에 대해 알려주는 사이트를 만들었다.

화면 뷰는 총4개로 첫화면 / 내 정보 / 상대 정보 / 결과 화면 으로 구성되어있다. 제플린으로 제작된 화면을 그대로 따라서 구현하려고 노력했다. 

 

사용 기술
- react(function components)
- style component
- react redux
- axios 

 

1. 컴포넌트 생성 & 폴더 구조

폴더구조

 

 

 

-> 액션은 삭제해도 되는데 실수로,, 남아있다,,,ㅎㅎ

 

->인풋 값을 저장할 리덕스 생성

 

-> 컴포넌트 폴더를 생성해서 필요한 페이지들을 만들어줌

    ( 폴더에 있는 jsx파일을 RootRouter를 통해 페이지 전환을 해줄 것이다.)

 

 

-> 모달창 폴더

 

-> RootRouter 

 

-> 이미지 소스 폴더

-> 전역에 적용할 스타일 컴포넌트 

 

 

->폰트

 

 

 

2. 페이지 Router 처리

🔷 페이지 흐름

  첫 화면 -> 내 정보 -> 상대 정보 -> 결과 화면

 

🔷 라우터 돔 설치

$ npm add react-router-dom

 

🔷 Root Router 파일 생성

     🚨 React Router가 v5 v6으로 버전이 변경되었다. 

          가장 큰 변화점은 Switch가 사라진 것 & useHistory가 아닌 useNavigator로 변경  된 것 이다. 

          프로젝트에 바로 적용을 시켜보았다. 기존 Switch는 여러개의 Route들을 묶어 둔 Routes로 변경되었다. 

//Rootrouter.js

import { Routes, Route } from "react-router-dom";

import MeinputPage from "../components/MeinputPage";
import YouinputPage from "../components/YouinputPage";
import ResultPage from "../components/ResultPage";
import InitialScreen from "../components/InitialScreen";

const Rootrouter = () => {
    return (
        <Routes>
            <Route path="/" element={<InitialScreen />} />
            <Route path="/mepage" element={<MeinputPage />} />
            <Route path="/youpage" element={<YouinputPage />} />
            <Route path="/result" element={<ResultPage />} />
        </Routes>
    );
};

export default Rootrouter;

더 많은 변경 사항을 참고 하고 싶으면 공식문서 링크에서 확인해보면 된다.                         

리액트 라우터 공식문서)) https://reactrouter.com/docs/en/v6/upgrading/v5 

 

React Router | Upgrading from v5

Declarative routing for React apps at any scale

reactrouter.com

 

🔷 src index.js 에 BrowserRouter로 App을 감싸주기

 

//src index.js
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter } from "react-router-dom";
import App from "./components/App";
import reportWebVitals from "./reportWebVitals";


ReactDOM.render(
   
        <BrowserRouter>
            <App />{" "}
        </BrowserRouter>

    document.getElementById("root")
);


reportWebVitals();

 

export 한 Rootrouter를 App.js에 적어주면 라우트 처리가 완료된다. 이제 해당 페이지에서 클릭이벤트가 발생했을 때 useNavigator함수를 사용해서 페이지가 넘어가는 것 처럼 보일 수 있게 만들어 주면 된다.  이부분은 각 컴포넌트 파일에서 다루도록 하겠다. 

//App.js
import Rootrouter from "../routers/Rootrouter";

function App() {
    return (
        <>
                <Rootrouter />
        </>
    );
}

export default App;

 

3. 글로벌 스타일 설정 -  스타일 컴포넌트 적용 

 

🔷 스타일 컴포넌트 설치

$ npm i styled-components

 

🔷 GlobalStyle

//GlobalStyle.js

import styled from "styled-components"; //설치된 스타일컴포넌트를 import해준다.
import check from "../images/check.jpg"; //배경으로 사용할 이미지

//전역에서 적용할 스타일 생성해주기 
export const GlobalStyle = styled.div`
    background-image: url(${check}); 
    width: 400px;
    height: 100%; //height는 페이지 내부 컨텐츠 양에 따라 달라질 수 있기 때문에 100% 로 해줌 
    padding-bottom: 50%; //배경 이미지가 높이가 짧은 파일이라서 padding을 줘서 좀 늘어나게 설정해줬다.
    margin: auto; // 페이지 가운데에 올 수 있게 설정 
    font-family: "MaruBuri"; // 글꼴(적용방법 링크 참조)
`;

폰트 적용하는 방법은 아래 링크 참고

https://velog.io/@lifeisbeautiful/React-%ED%8F%B0%ED%8A%B8%EC%9E%85%ED%98%80%EB%B3%B4%EA%B8%B0

 

이제 App.js 에서 Rootrouter 외부를 감싸주면 안에있는 컴포넌트에 GlobalStyle이 적용된다.

//App.js

import Rootrouter from "../routers/Rootrouter";
import { GlobalStyle } from "../styleComponent/GlobalStyle"; //글로벌 스타일 임포트하기
import "../App.css";
function App() {
    return (
        <>
            <GlobalStyle>
                <Rootrouter />
            </GlobalStyle>
        </>
    );
}

export default App;

 

 

4. InitialScreen - 첫 화면 만들기

 

GlobalStyle을 적용해서 배경과 전체폭, 높이만 정해져 있는 상태에 옆에 보이

는 나머지 내용들을 추가하고 기능을 구현하고자 한다. 

 

 <기능 구현>

- i 아이콘 Click  ⏩ 출처 정보 담은 모달창 띄우기

- 시작하기 버튼 Click  ⏩ 다음페이지 이동

- 테스트 공유 Click  ⏩ url 복사 + alert창 띄우기

- 참여인원 API get ⏩ 화면에 띄우기 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

🔷 i 아이콘 Click 출처 정보 담은 모달창 띄우기

 

  1) IconModal component  생성

//IconModal.jsx 

import React from "react";
import styled from "styled-components";

let IconModal = (props) => {
    const onClick = () => {
        props.setInfoIconModal(false);
    };
//배경클릭시 InitialScreen의 infoIconModal의 값이 true에서 false로 변경되면서 창 꺼짐 이벤트

    return (
        <IconModalStyle> //스타일 컴포넌트 
            <ModalBack> // 클릭시 검은색 반투명 배경 추가 될 것 
                <div className="modalBack" onClick={onClick}></div> 
                <div className="IconModal">
                    <div className="IconModalInfo">
                        <div className="question">
                            <strong>문의:</strong> codnjs779@naver.com
                        </div>

                        <div className="source">
                            <strong>출처:</strong>
                            <div>- MBTI 유형별 궁합</div>
                            <div>- 네이버 지식백과</div>
                            <div>- 별자리 궁합표</div>
                        </div>
                    </div>
                </div>
            </ModalBack>
        </IconModalStyle>
    );
};

 

2) InitialScreen component  생성

//InitialScreen.jsx
import React from "react";
import IconModal from "../modal/IconModal";
//앞서 만들어둔 모달창을 InitialScreen 에서 사용하기 위해 컴포넌트를 import 해서 사용해줬다. 

//fontawesome
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; 
 
 
const InitialScreen = () => {
   const [infoIconModal, setInfoIconModal] = useState(false);
   
       const onClick = () => {
        setInfoIconModal(!infoIconModal);
    };
    // 아이콘을 클릭할 때 창이 사라질 수 있는 기능
 
 // 삼항연산자를 사용해서 false일때는 보이지 않고 true일때만 모달이 띄워질 수 있게 해줌
// IconModal component에 setInfoIconModal을 props로 넘겨서 IconModal에서 infoIconModal 값을 조정할 수 있게 함.

 return (
            <>
                {infoIconModal === true ? <IconModal setInfoIconModal={setInfoIconModal} /> : null}   
                <div className="infoIcon">
                    <FontAwesomeIcon icon={faInfoCircle} onClick={onClick} />
                </div>
    </>
    );
    }

폰트 어썸 적용하는 방법은 아래 링크 참고

https://jae04099.tistory.com/entry/React-%EB%A6%AC%EC%95%A1%ED%8A%B8%EC%97%90-font-awesome-%EC%95%84%EC%9D%B4%EC%BD%98-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B

 

🔷 시작하기 버튼 Click  다음페이지 이동 

//InitialScreen.jsx
import { Link } from "react-router-dom";

<Link to="mepage">
         <button className="blackBtn">시작하기</button>
 </Link>

Rooter 처리를 해줄 때 "내 정보" 입력하는 페이지의 주소를 -> "/mepage"로 설정해줬다. Link를 import 하고 해당 주소를 Link to 에 써주면 된다. 이 방법 말고 useNavigator로 할 수도 있는데 이건 다음 페이지부터 적용시켰다. 

 

 

🔷 테스트 공유Click url 복사 + alert창 띄우기

$ npm install --save react-copy-to-clipboard
//InitialScreen.jsx
import { CopyToClipboard } from "react-copy-to-clipboard";

const url = "https://chemistry-test.co.kr";

return(
<CopyToClipboard text={url}>
    <button className="yellowBtn" onClick={() => {alert("링크 복사가 완료되었습니다!"); }}>
          테스트 공유
    </button>
 </CopyToClipboard>
 )

CopyToClipboard를 사용해서 링크를  복사할 수 있는 기능을 손쉽게 구현할 수 있다.

 

🔷 참여인원 API get ⏩ 화면에 띄우기 

$ npm install axios
//InitialScreen.jsx
import React, { useState, useEffect } from "react";
import axios from "axios";

const [data, setData] = useState();
  //axios response 값이 들어갈 자리 마련
  
  useEffect(() => {
        axios
            .get("https://api주소")
            .then((response) => {
                setData(response.data.result.count);
            })
            .catch((error) => {
                console.log(error);
            });
    }, []);
    
//받아온 데이터 활용하기 
    return(
     <div className="countNumber">
                        현재까지 <span>{data}</span>명이 참여했어요
                    </div>
)

 

🚩 스타일 컴포넌트까지 작성한 InitialScreen  &  IconModal 최종코드 

//InitialScreen.jsx

import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { CopyToClipboard } from "react-copy-to-clipboard";
import axios from "axios";

//img
import heart from "../images/inheart.png";

//style
import styled from "styled-components";
import IconModal from "../modal/IconModal";

//fontawesome
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";

const InitialScreen = () => {
    const [infoIconModal, setInfoIconModal] = useState(false);
    const [data, setData] = useState();

    const url = "https://chemistry-test.co.kr";
    const onClick = () => {
        setInfoIconModal(!infoIconModal);
    };

    useEffect(() => {
        axios
            .get("https://dlsrksrndgkq.link/count")
            .then((response) => {
                setData(response.data.result.count);
            })
            .catch((error) => {
                console.log(error);
            });
    }, []);

    return (
        <InitialContentsBoxStyle>
            <div className="initialContentsBox">
                {infoIconModal === true ? <IconModal setInfoIconModal={setInfoIconModal} /> : null}
                <div className="infoIcon">
                    <FontAwesomeIcon icon={faInfoCircle} onClick={onClick} />
                </div>
                <TitleAndcontentsDesign>
                    <h1 className="title">우리의 모든 것</h1>
                    <div className="contents">mbti 별자리 띠 혈액형 모든 궁합을 한번에 확인해보세요!</div>
                    <div className="heartIcon">
                        <img src={heart} alt="img" />
                    </div>
                    <div className="countNumber">
                        현재까지 <span>{data}</span>명이 참여했어요
                    </div>
                </TitleAndcontentsDesign>

                <div className="buttonSet">
                    <ButtonDesign>
                        <Link to="mepage">
                            <button className="blackBtn">시작하기</button>
                        </Link>

                        <CopyToClipboard text={url}>
                            <button
                                className="yellowBtn"
                                onClick={() => {
                                    alert("링크 복사가 완료되었습니다!");
                                }}
                            >
                                테스트 공유
                            </button>
                        </CopyToClipboard>
                    </ButtonDesign>
                </div>
            </div>
        </InitialContentsBoxStyle>
    );
};

const InitialContentsBoxStyle = styled.div`
    .infoIcon {
        width: 18pt;
        height: 18pt;
        cursor: pointer;
        margin: 0 auto;
        position: relative;
        left: 160px;
        top: 20px;
    }
`;

const TitleAndcontentsDesign = styled.div`
    .title {
        text-align: center;
        font-size: 2.1rem;
        width: 230px;
        margin: 0 auto;
        position: relative;
        top: 100px;
    }

    .contents {
        text-align: center;
        font-size: 1.1rem;
        width: 250px;
        margin: 0 auto;
        color: rgb(153 153 153);
        position: relative;
        top: 120px;
    }
    .heartIcon {
        text-align: center;
    }
    .heartIcon img {
        width: 250px;
        margin: 0 auto;
        position: relative;
        top: 160px;
    }

    .countNumber {
        text-align: center;
        margin: 0 auto;
        position: relative;
        top: 170px;
    }

    span {
        font-weight: 800;
    }
`;

//버튼 디자인은 계속 재활용 할 예정이라 export 처리해줌 

export const ButtonDesign = styled.div`
    text-align: center;
    display: flex;
    position: relative;
    top: 230px;
    flex-direction: column;
    .blackBtn,
    .yellowBtn {
        width: 330px;
        height: 70px;
        font-size: 1.3rem;
        text-align: center;
        line-height: 22pt;
        font-weight: bold;
        border-radius: 13pt;
        border: none;
        cursor: pointer;
        margin: 0 auto;
    }

    .blackBtn {
        background-color: rgb(34 34 34);
        color: rgb(255 255 255);
    }
    .yellowBtn {
        margin-top: 20px;
        background-color: rgb(255 234 70);
        color: rgb(51 51 51);
    }
`;
export default InitialScreen;

 

//IconModal.jsx
import React from "react";
import styled from "styled-components";

let IconModal = (props) => {
    const onClick = () => {
        props.setInfoIconModal(false);
    };
    return (
        <IconModalStyle>
            <ModalBack>
                <div className="modalBack" onClick={onClick}></div>
                <div className="IconModal">
                    <div className="IconModalInfo">
                        <div className="question">
                            <strong>문의:</strong> codnjs779@naver.com
                        </div>

                        <div className="source">
                            <strong>출처:</strong>
                            <div>- MBTI 유형별 궁합</div>
                            <div>- 네이버 지식백과</div>
                            <div>- 별자리 궁합표</div>
                        </div>
                    </div>
                </div>
            </ModalBack>
        </IconModalStyle>
    );
};

const IconModalStyle = styled.div`
    .IconModal {
        background-color: rgb(255 255 255);
        position: absolute;
        z-index: 2;
        width: 230pt;
        height: 150pt;
        border-radius: 15pt;
        opacity: 1;
        margin-left: 10pt;
        top: 87pt;
    }

    .IconModalInfo {
        padding: 15pt;
        font-size: 15pt;
    }
    .source {
        padding-top: 20pt;
    }
`;

export const ModalBack = styled.div`
    .modalBack {
        position: absolute;
        z-index: 1;
        width: 400px;
        height: 100%;
        background-color: #202020;
        opacity: 0.5;
        margin: auto;
    }
`;

export default IconModal;

🚩 InitialScreen 기능 구현 완성