css에 조건문 걸기
@media(max-width)로 조건 걸기
App.css
btnsize 클래스 조건 주기
/* 노멀라이즈 */
* {
box-sizing: border-box;
padding: 0px;
margin: 0px;
}
a {
color: inherit;
text-decoration: none;
}
ul,
li {
list-style: none;
color: inherit;
}
button {
cursor: pointer;
outline: none;
border: none;
}
.app {
background-color: #a6c0d5;
}
.content-box {
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
/* width: 480px; */
margin: 0px auto;
}
/* 화면 크기에 따른 버튼 크기 변경 -> 일단 나중에 */
.button {
/* width: 165px; */
font-size: 18px;
font-weight: bold;
color: black;
background-color: #fbe400;
margin: 20px;
padding: 10px 40px;
border-radius: 5px;
box-shadow: grey 2px 2px 4px;
}
p {
padding-top: 20px;
}
.sub {
width: 390px;
height: 261px;
margin: 60px 0;
}
.progress-bar {
width: 480px;
background-color: #ffffff;
height: 30px;
border-radius: 12px;
}
.percent {
/* width 값을 바꾸면서 프로그레스바 채움 */
/* width: 30px; */
background-color: #fbe400;
height: 30px;
border-radius: 10px;
}
@media (max-width: 600px) {
.bntsize {
width: 400px;
height: auto;
}
}
@media (max-width: 400px) {
.bntsize {
width: 300px;
height: auto;
}
}
App.js
버튼에 bntsize 클래스 주기
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";
import {
Routes,
Route,
useNavigate,
useLocation,
useParams,
} from "react-router-dom";
const question = [
{
text1: "당연하지! 어디서 할지 고민 중이야!",
text2: "그냥 맛있는거 먹으러 갈까 생각 중이야!",
re1: "E",
re2: "I",
},
{
text1: "영화 완전 재밌었어! 너도 한번 봐봐!",
text2: "좀비가 너무 리얼했어. 실제 상황이면 난 바로 죽었을거야...",
re1: "S",
re2: "N",
},
{
text1: "무슨 꽃 샀어? 향은 좋아?",
text2: "왜 우울해? 무슨 일 있어?",
re1: "T",
re2: "F",
},
{
text1: "지금 PPT 만드는 중이니까 아마 한 2시간 뒤면 끝날거 같아!",
text2: "모르겠어. 근데 지금 PPT 만들고 있어!",
re1: "J",
re2: "P",
},
{
text1: "그래! 역시 사람 많고 유명한 벚꽃 명소가 예쁘겠지 어디로 갈까?",
text2: "그래! 사람 적은 볓꽃 명소 한번 찾아볼까?",
re1: "E",
re2: "I",
},
{
text1: "지구는 멸망하지 않아!",
text2: "일단 가장 좋아하는 음식부터 먹으러 갈거야!",
re1: "S",
re2: "N",
},
{
text1: "드라이샴푸? 마른 머리에 비비면 되는건가? 완전 티 안나!",
text2: "어제 무슨 일 있었어? 지금은 피곤한거 좀 괜찮아?",
re1: "T",
re2: "F",
},
{
text1:
"우선 2시에 도착하니까 버스 타고 숙소가서 체크인하고 늦은 점심 먹을까?",
text2: "글쎄... 너는 가고 싶은데 없어? 난 현지 맛집이면 다 좋아!",
re1: "J",
re2: "P",
},
{
text1: "아니! 나 지금 밖이야! 친구랑 있어. 무슨 일 있어?",
text2: "응! 나 지금 넷플릭스 보고 있어! 무슨 일 있어?",
re1: "E",
re2: "I",
},
{
text1: "이해는 안가는데 시험번위고 종요하다니까 그냥 외우려고!",
text2: "이따가 선생님한테 가서 다시 물어보려고!",
re1: "S",
re2: "N",
},
{
text1: "무슨 시험 봤어? 다음 시험은 언제야? 괜찮아?",
text2: "다음에 꼭 붙을거야! 다음 시험을 기약하자! 너무 슬퍼하지마!",
re1: "T",
re2: "F",
},
{
text1: "진짜로? 왜? 갑자기? 다른 사람들은 뭐래?",
text2: "오! 갑자기 왜? 그럼 조는 어떻게 정한대?",
re1: "J",
re2: "P",
},
];
function Main() {
const navigation = useNavigate();
const { setDispatchType } = React.useContext(StoreContext);
React.useEffect(() => {
setDispatchType({
code: "tmpStorage",
});
}, []);
return (
<div className="app">
<div className="content-box">
<div className="img-box">
<img
src="https://kakaofriendsmbti.netlify.app/static/media/00.88f71908.png"
alt="메인페이지이미지"
/>
</div>
<button
className="button bntsize"
onClick={() => {
navigation("/on1");
}}
>
시작하기
</button>
<p>MADE BY @geenee</p>
</div>
</div>
);
}
const ProgressBar = (props) => {
// 서브페이지는 총 12개
const width = (480 / 12) * props.step;
return (
<div className="progress-bar">
<div className="percent" style={{ width: width }}></div>
</div>
);
};
function Sub() {
const { setDispatchType } = React.useContext(StoreContext);
const { seq } = useParams();
const imgseq = seq.length === 1 ? "0" + seq : seq;
const srcurl = `https://kakaofriendsmbti.netlify.app/images/${imgseq}-01.png`;
const seqn = parseInt(seq);
const 데이터저장 = (props) => {
const value = props === 1 ? question[seqn - 1].re1 : question[seqn - 1].re2;
setDispatchType({
code: "answer",
params: { value: value, page: seqn },
});
};
return (
<div className="app">
<div className="content-box">
<ProgressBar step={seq} />
<div className="img-box sub">
<img src={srcurl} alt="서브페이지이미지" />
</div>
<button className="button bntsize" onClick={() => 데이터저장(1)}>
{question[seqn - 1].text1}
</button>
<button className="button bntsize" onClick={() => 데이터저장(2)}>
{question[seqn - 1].text2}
</button>
</div>
</div>
);
}
function Result() {
const navigation = useNavigate();
const { state } = useLocation();
const { setDispatchType } = React.useContext(StoreContext);
const [result, setResult] = React.useState(undefined);
//결과 서버로 전송
const MBTISend = async () => {
await axios({
url: "http://localhost:5000/mbti",
method: "GET",
responseType: "json",
params: state,
})
.then((res) => {
// console.log(res.data);
setResult(res.data);
})
.catch((e) => {
console.log("error!!", e);
});
};
React.useEffect(() => {
MBTISend();
}, []);
if (result === undefined) {
return <div></div>;
}
return (
<div className="app">
<div className="content-box">
<div className="img-box">
<img src={result.content} alt="결과페이지이미지" />
</div>
<button
className="button bntsize"
onClick={() => {
setDispatchType({
code: "dataReset",
});
navigation("/on1");
}}
>
다시하기
</button>
</div>
</div>
);
}
const StoreContext = React.createContext({});
function App() {
const navigation = useNavigate();
const [dispatch, setDispatchType] = React.useState({
code: null,
params: null,
});
const [mbti, setMbti] = React.useState([
{ E: 0, I: 0 },
{ N: 0, S: 0 },
{ T: 0, F: 0 },
{ J: 0, P: 0 },
]);
React.useEffect(() => {
switch (dispatch.code) {
case "answer":
// mbti 결과 저장
const { value, page } = dispatch.params;
const clonembti = [...mbti];
const findIndex = clonembti.findIndex((item) => {
return item[value] !== undefined;
});
clonembti[findIndex][value]++;
// console.log(clonembti);
setMbti(clonembti);
//로컬 스토리지 저장
localStorage.setItem("MBTI", JSON.stringify(clonembti));
localStorage.setItem("PAGE", page);
//페이지 이동
if (page < 12) {
navigation(`/on${page + 1}`);
} else {
navigation("/result", { state: mbti });
}
break;
case "dataReset":
setMbti([
{ E: 0, I: 0 },
{ N: 0, S: 0 },
{ F: 0, T: 0 },
{ J: 0, P: 0 },
]);
break;
case "tmpStorage":
const tmpMbti = localStorage.getItem("MBTI");
const tmpPage = localStorage.getItem("PAGE");
// console.log(tmpMbti, tmpPage);
if (tmpPage === "12") {
localStorage.removeItem("MBTI");
localStorage.removeItem("PAGE");
return;
}
if (tmpMbti && tmpPage) {
const tmpMbtiarray = JSON.parse(tmpMbti);
setMbti(tmpMbtiarray);
const currentpage = Number(tmpPage);
navigation(`/on${currentpage + 1}`);
}
break;
default:
break;
}
}, [dispatch]);
return (
<StoreContext.Provider value={{ setDispatchType }}>
<Routes>
<Route exact path="/" element={<Main />} />
<Route exact path="/on:seq" element={<Sub />} />
<Route exact path="/result" element={<Result />} />
</Routes>
</StoreContext.Provider>
);
}
export default App;
테스트 질문 중 뒤로가기 못하게
뒤로가기하면 메인페이지 혹인 이전 페이지로 넘거가게~
[React] 뒤로가기 이벤트를 탐지하는 방법 (react router v6 적용 가능)
React router v5에서는 useHistory 의 return value인 history의 history.listen() 으로 뒤로가기 이벤트를 탐지하는 예제가 많이 나온다. 그러나 최근에 React router가 v6 업데이트 되면서 useHistory가 없어지며 history.listen을 사용하시던 분들과 뒤로가기 이벤트 탐지를 검색하시던 분들이 어려움을 겪었을 것이다. 필자 또한 이 방법을 찾느라 + 그냥 코드를 잘못 적었는데 몰라서 뒤로가기 이벤트 탐지 기능을 구현하는 데 애를 먹었다.
history.tsimport { createBrowserHistory } from "history"; export const history = createBrowserHistory();
먼저 react-router에 history가 없어졌으니 history 라이브러리에서 가져오자. 패키지 매니저에 따라 yarn add history나 npm i history로 추가하면 된다.
추가했으면 createBrowerHistory()로 history 객체를 생성한다
page.tsximport { history } from "some/directory/history" useEffect(() => { const listenBackEvent = () => { // 뒤로가기 할 때 수행할 동작을 적는다 }; const unlistenHistoryEvent = history.listen(({ action }) => { if (action === "POP") { listenBackEvent(); } }); return unlistenHistoryEvent; }, [ // effect에서 사용하는 state를 추가 ]);
그 다음은 아까 만들었던 history객체를 import만 하면 기존에 useHistory의 history를 사용하는 것과 똑같다. history의 update event를 listen하여 action이 "POP"일 때 (뒤로가기. 쌓아온 사용자 history stack을 pop 하는 거라서 뒤로가기다) 원하는 동작을 수행해야 한다.
이 때 listen으로 건 이벤트를 해제해야 하니 주의해야 한다. 그렇지 않으면 이벤트가 useEffect가 실행될 때마다 추가되기만 해서 같은 이벤트 여러개가 동작할 것이다. history.listen을 해제하는 방법은 history.listen이 return하는 함수를 호출하면 된다. 이 함수를 effect cleanup에 넣어서 다른 state를 가진 이벤트가 실행되기 전에 이전 이벤트가 해제되도록 하자.
javascript history 삭제하기 는 불가능하다.
구글링 결과 javascript 로 history를 제어할 수 없는 듯 하다.
즉 클라이언트에서 권한이 없다는 것으로 예상된다.
미흡한 영어실력이라 해석이 잘못 됬을지도 모르지만 안되는것 처럼 보인다.
하지만 방법이 없는 것은 아닌듯 하다 ^^
나같은 경우는 다음과 같이 해결했다.
그럼 먼저 history 에 url이 남는 경우들을 살펴본다.
submit으로 form전송을 한 다음 server단에서 redirect 로 이동, 클라이언트에서 location.href 사용 등이 histroy에 남는다.
②페이지들에서의 ③페이지로 이동은 submit으로 form 전송을 한 뒤 redirect 했거나, 클라이언트에서 location.href 를 사용했을 것이다.
그렇다면 history에 남지 않고 페이지를 이동할 수 있을까 ?
클라이언트에서 location.replace() 를 사용하면 남지 않는다.
location.replace() 는 클라이언트에서 페이지를 이동시키는 함수이다. 그런 점에서 보면 location.href와 비슷하다고 생각 될 수 있는데 확실히 다르다.
location.replace() 는 history에 남지 않고 페이지를 이동하는 효과를 볼 수 있다.
그렇다면 location.href 는 location.replace() 로 대체할 수 있다.
다음으로 전송방식에 대해서 생각해본다.
submit 으로 form 전송 한 뒤에 서버에서 redirect 하는 방법 말고 다른 방법이 있는가?
비동기 ajax 방식이 존재한다.
비동기로 서버에 갔다가 특정 작업을 완료 한 후 다시 클라이언트로 돌아오는 방법이다.
클라이언트로 돌아왔기 때문에 redirect 가 아닌 location.href 또는 location.replace() 를 사용하여 페이지 전환을 해야 한다.
submit 은 ajax로 대체가 가능하다.
정리해본다.
먼저 ②페이지들에서 페이지를 이동하는 방식을 변경한다.
submit > redirect >>> 변경 >>> ajaxSubmit > location.replace()
또는
location.href >>> 변경 >>> location.replace()
다음과 같은 결과를 얻을 수 있다.
페이지 이동 : ①페이지 > ②페이지 > ③페이지
뒤로가기 이동 : ③페이지 > ①페이지
history를 삭제할 수는 없지만 history에 남기지 않는 방법으로 우회하여 해결 할 수는 있다.
- useNavigate 훅
useNavigate() 를 호출하면, 경로 이동 처리를 할 수 있는 "함수"를 반환한다.
이 함수를 이용해 뒤로 이동, 경로 이동 등을 처리하면 된다.
replace 옵션을 통해 history에 이력을 남길지 여부를 설정할 수 있다.
사용방법은 아래 코드를 보면 너무나 명확하다.import { useNavigate } from 'react-router-dom'; export default function Test() { const navigate = useNavigate(); return ( <div> <button onClick={()=>{navigate(-1)}}>history 뒤로 이동</button> <button onClick={()=>{navigate("/admin")}}>절대 경로 이동</button> <button onClick={()=>{navigate("../content")}}>상대 경로 이동</button> <button onClick={()=>{navigate("/admin", { replace: true })}}>history 이력 안남김</button> </div> ); }
- 더 이상의 자세한 설명은 생략한다. 왜냐하면 없기 때문이다.
- 혹시 페이지 이동시, 파라미터를 전달하고 싶다면, 아래 글을 참고하자.
App.js
import React from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";
import {
Routes,
Route,
useNavigate,
useLocation,
useParams,
} from "react-router-dom";
// import { createBrowserHistory } from "history";
// const history = createBrowserHistory();
const question = [
{
text1: "당연하지! 어디서 할지 고민 중이야!",
text2: "그냥 맛있는거 먹으러 갈까 생각 중이야!",
re1: "E",
re2: "I",
},
{
text1: "영화 완전 재밌었어! 너도 한번 봐봐!",
text2: "좀비가 너무 리얼했어. 실제 상황이면 난 바로 죽었을거야...",
re1: "S",
re2: "N",
},
{
text1: "무슨 꽃 샀어? 향은 좋아?",
text2: "왜 우울해? 무슨 일 있어?",
re1: "T",
re2: "F",
},
{
text1: "지금 PPT 만드는 중이니까 아마 한 2시간 뒤면 끝날거 같아!",
text2: "모르겠어. 근데 지금 PPT 만들고 있어!",
re1: "J",
re2: "P",
},
{
text1: "그래! 역시 사람 많고 유명한 벚꽃 명소가 예쁘겠지 어디로 갈까?",
text2: "그래! 사람 적은 볓꽃 명소 한번 찾아볼까?",
re1: "E",
re2: "I",
},
{
text1: "지구는 멸망하지 않아!",
text2: "일단 가장 좋아하는 음식부터 먹으러 갈거야!",
re1: "S",
re2: "N",
},
{
text1: "드라이샴푸? 마른 머리에 비비면 되는건가? 완전 티 안나!",
text2: "어제 무슨 일 있었어? 지금은 피곤한거 좀 괜찮아?",
re1: "T",
re2: "F",
},
{
text1:
"우선 2시에 도착하니까 버스 타고 숙소가서 체크인하고 늦은 점심 먹을까?",
text2: "글쎄... 너는 가고 싶은데 없어? 난 현지 맛집이면 다 좋아!",
re1: "J",
re2: "P",
},
{
text1: "아니! 나 지금 밖이야! 친구랑 있어. 무슨 일 있어?",
text2: "응! 나 지금 넷플릭스 보고 있어! 무슨 일 있어?",
re1: "E",
re2: "I",
},
{
text1: "이해는 안가는데 시험번위고 종요하다니까 그냥 외우려고!",
text2: "이따가 선생님한테 가서 다시 물어보려고!",
re1: "S",
re2: "N",
},
{
text1: "무슨 시험 봤어? 다음 시험은 언제야? 괜찮아?",
text2: "다음에 꼭 붙을거야! 다음 시험을 기약하자! 너무 슬퍼하지마!",
re1: "T",
re2: "F",
},
{
text1: "진짜로? 왜? 갑자기? 다른 사람들은 뭐래?",
text2: "오! 갑자기 왜? 그럼 조는 어떻게 정한대?",
re1: "J",
re2: "P",
},
];
function Main() {
const navigation = useNavigate();
const { setDispatchType } = React.useContext(StoreContext);
React.useEffect(() => {
setDispatchType({
code: "tmpStorage",
});
}, []);
return (
<div className="app">
<div className="content-box">
<div className="img-box">
<img
src="https://kakaofriendsmbti.netlify.app/static/media/00.88f71908.png"
alt="메인페이지이미지"
/>
</div>
<button
className="button bntsize"
onClick={() => {
navigation("/on1", { replace: true });
}}
>
시작하기
</button>
<p>MADE BY @geenee</p>
</div>
</div>
);
}
const ProgressBar = (props) => {
// 서브페이지는 총 12개
const width = (480 / 12) * props.step;
return (
<div className="progress-bar">
<div className="percent" style={{ width: width }}></div>
</div>
);
};
function Sub() {
const { setDispatchType } = React.useContext(StoreContext);
const { seq } = useParams();
const imgseq = seq.length === 1 ? "0" + seq : seq;
const srcurl = `https://kakaofriendsmbti.netlify.app/images/${imgseq}-01.png`;
const seqn = parseInt(seq);
const 데이터저장 = (props) => {
const value = props === 1 ? question[seqn - 1].re1 : question[seqn - 1].re2;
setDispatchType({
code: "answer",
params: { value: value, page: seqn },
});
};
return (
<div className="app">
<div className="content-box">
<ProgressBar step={seq} />
<div className="img-box sub">
<img src={srcurl} alt="서브페이지이미지" />
</div>
<button className="button bntsize" onClick={() => 데이터저장(1)}>
{question[seqn - 1].text1}
</button>
<button className="button bntsize" onClick={() => 데이터저장(2)}>
{question[seqn - 1].text2}
</button>
</div>
</div>
);
}
function Result() {
const navigation = useNavigate();
const { state } = useLocation();
const { setDispatchType } = React.useContext(StoreContext);
const [result, setResult] = React.useState(undefined);
//결과 서버로 전송
const MBTISend = async () => {
await axios({
url: "http://localhost:5000/mbti",
method: "GET",
responseType: "json",
params: state,
})
.then((res) => {
// console.log(res.data);
setResult(res.data);
})
.catch((e) => {
console.log("error!!", e);
});
};
React.useEffect(() => {
MBTISend();
}, []);
if (result === undefined) {
return <div></div>;
}
return (
<div className="app">
<div className="content-box">
<div className="img-box">
<img src={result.content} alt="결과페이지이미지" />
</div>
<button
className="button bntsize"
onClick={() => {
setDispatchType({
code: "dataReset",
});
navigation("/on1", { replace: true });
}}
>
다시하기
</button>
</div>
</div>
);
}
const StoreContext = React.createContext({});
function App() {
const navigation = useNavigate();
const [dispatch, setDispatchType] = React.useState({
code: null,
params: null,
});
const [mbti, setMbti] = React.useState([
{ E: 0, I: 0 },
{ N: 0, S: 0 },
{ T: 0, F: 0 },
{ J: 0, P: 0 },
]);
React.useEffect(() => {
switch (dispatch.code) {
case "answer":
// mbti 결과 저장
const { value, page } = dispatch.params;
const clonembti = [...mbti];
const findIndex = clonembti.findIndex((item) => {
return item[value] !== undefined;
});
clonembti[findIndex][value]++;
// console.log(clonembti);
setMbti(clonembti);
//로컬 스토리지 저장
localStorage.setItem("MBTI", JSON.stringify(clonembti));
localStorage.setItem("PAGE", page);
//페이지 이동
if (page < 12) {
navigation(`/on${page + 1}`, { replace: true });
} else {
navigation("/result", { replace: true, state: mbti });
}
break;
case "dataReset":
setMbti([
{ E: 0, I: 0 },
{ N: 0, S: 0 },
{ F: 0, T: 0 },
{ J: 0, P: 0 },
]);
break;
case "tmpStorage":
const tmpMbti = localStorage.getItem("MBTI");
const tmpPage = localStorage.getItem("PAGE");
// console.log(tmpMbti, tmpPage);
if (tmpPage === "12") {
localStorage.removeItem("MBTI");
localStorage.removeItem("PAGE");
return;
}
if (tmpMbti && tmpPage) {
const tmpMbtiarray = JSON.parse(tmpMbti);
setMbti(tmpMbtiarray);
const currentpage = Number(tmpPage);
navigation(`/on${currentpage + 1}`, { replace: true });
}
break;
default:
break;
}
}, [dispatch]);
return (
<StoreContext.Provider value={{ setDispatchType }}>
<Routes>
<Route exact path="/" element={<Main />} />
<Route exact path="/on:seq" element={<Sub />} />
<Route exact path="/result" element={<Result />} />
</Routes>
</StoreContext.Provider>
);
}
export default App;
페이지 넘어갈때마다 보이는 주소 안보이게 하기~
App을 BrowerRouter가 아닌 MemoryRouter로 감싸면 path가 나타나지않음
근데 이게 맞는 방식인지는 모르겠음?
MemoryRouter는 브라우저가 아닌 환경에서 쓴다고 하는거 보니까..아닌것같고
일단 더 공부가 필요할듯~
Router 이론
url 감추기는 다시 공부할게여..
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
import { MemoryRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<MemoryRouter>
<App />
</MemoryRouter>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();