[수업 목표]
[목차]
💡 모든 토글을 열고 닫는 단축키 Windows : `Ctrl` + `alt` + `t` Mac : `⌘` + `⌥` + `t`Box를 동그랗게 만들고,

...
const Box = styled.div`
width: 100px;
height: 100px;
border-radius: 50px;
background: green;
animation: ${boxFade} 2s 1s infinite linear alternate;
`;
...
position을 준 다음,
...
const Box = styled.div`
width: 100px;
height: 100px;
border-radius: 50px;
background: green;
position: absolute;
top: 20px;
left: 20px;
animation: ${boxFade} 2s 1s infinite linear alternate;
`;
...
위 아래로 움직이게 해보자!
```jsx
...
// 이런식으로 동시에 여러가지 애니메이션을 넣어줄 수 있어요!
const boxFade = keyframes`
0% {
opacity: 1;
top: 20px;
}
50% {
opacity: 0;
top: 400px;
}
100% {
opacity: 1;
top: 20px;
}
`;
...
```
😉 이거 재미있죠? styled-components와 keyframes로 할 수 있는 건 훨씬 많아요!
여러 가지 애니메이션 효과를 찾아서 넣어보세요. 즐거울거예요.

import React from "react";
import styled from "styled-components";
import { Route, Switch } from "react-router-dom";
import {useDispatch} from "react-redux";
import {createBucket} from "./redux/modules/bucket";
// BucketList 컴포넌트를 import 해옵니다.
// import [컴포넌트 명] from [컴포넌트가 있는 파일경로];
import BucketList from "./BucketList";
import Detail from "./Detail";
import NotFound from "./NotFound";
import Progress from "./Progress";
function App() {
const [list, setList] = React.useState([
"영화관 가기",
"매일 책읽기",
"수영 배우기",
]);
const text = React.useRef(null);
const dispatch = useDispatch();
const addBucketList = () => {
// 스프레드 문법! 기억하고 계신가요? :)
// 원본 배열 list에 새로운 요소를 추가해주었습니다.
// setList([...list, text.current.value]);
dispatch(createBucket(text.current.value));
};
return (
<div className="App">
<Container>
<Title>내 버킷리스트</Title>
<Progress/>
<Line />
{/* 컴포넌트를 넣어줍니다. */}
{/* <컴포넌트 명 [props 명]={넘겨줄 것(리스트, 문자열, 숫자, ...)}/> */}
<Switch>
<Route path="/" exact>
<BucketList list={list} />
</Route>
<Route path="/detail/:index">
<Detail />
</Route>
<Route>
<NotFound />
</Route>
</Switch>
</Container>
{/* 인풋박스와 추가하기 버튼을 넣어줬어요. */}
<Input>
<input type="text" ref={text} />
<button onClick={addBucketList}>추가하기</button>
</Input>
</div>
);
}
const Input = styled.div`
max-width: 350px;
min-height: 10vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;
const Container = styled.div`
max-width: 350px;
min-height: 60vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;
const Title = styled.h1`
color: slateblue;
text-align: center;
`;
const Line = styled.hr`
margin: 16px 0px;
border: 1px dotted #ddd;
`;
export default App;import React from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
const Progress = (props) => {
const bucket_list = useSelector((state) => state.bucket.list);
console.log(bucket_list);
let count = 0;
bucket_list.map((b, idx) => {
if (b.completed) {
count++;
}
});
console.log(count);
return (
<ProgressBar>
<HighLight width={(count / bucket_list.length) * 100 + "%"} />
</ProgressBar>
);
};
const ProgressBar = styled.div`
background: #eee;
width: 100%;
height: 40px;
`;
const HighLight = styled.div`
background: orange;
transition: 1s;
width: ${(props) => props.width};
height: 40px;
`;
export default Progress;
👉 웹은 **요청과 응답**으로 굴러갑니다!
클라이언트가 서버에게 요청, 서버가 클라이언트에게 응답!









//firebase.js
import { initializeApp } from "firebase/app";
import { getFirestore } from "firebase/firestore";
const firebaseConfig = {
// firebase 설정과 관련된 개인 정보
};
// firebaseConfig 정보로 firebase 시작
initializeApp(firebaseConfig);
// firebase의 firestore 인스턴스를 변수에 저장
const db = getFirestore();
// 필요한 곳에서 사용할 수 있도록 내보내기
export { db };
import {db} from "./firebase";...
const bucket = firestore.collection("buckets");
React.useEffect(() => {
console.log(db);
}, []);
...[숙제]
랭킹화면, 한마디 화면 만들기
퀴즈 페이지에 프로그래스바 만들기
파이어스토어 만들고 데이터 넣기!


// Actions
// 유저 이름을 바꾼다
const ADD_USER_NAME = "rank/ADD_USER_NAME";
// 유저 메시지를 바꾼다
const ADD_USER_MESSAGE = "rank/ADD_USER_MESSAGE";
// 랭킹정보를 추가한다
const ADD_RANK = "rank/ADD_RANK";
// 랭킹정보를 가져온다
const GET_RANK = "rank/GET_RANK";
const initialState = {
user_name: "",
user_message: "",
user_score: "",
score_text: {
60: "우린 친구! 앞으로도 더 친하게 지내요! :)",
80: "우와! 우리는 엄청 가까운 사이!",
100: "둘도 없는 단짝이에요! :)",
},
ranking: [
{ score: 40, name: "임민영", message: "안녕 르탄아!" },
],
};
// Action Creators
export const addUserName = (user_name) => {
return { type: ADD_USER_NAME, user_name };
};
export const addUserMessage = (user_message) => {
return { type: ADD_USER_MESSAGE, user_message };
};
export const addRank = (rank_info) => {
return { type: ADD_RANK, rank_info };
};
export const getRank = (rank_list) => {
return { type: GET_RANK, rank_list };
};
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "rank/ADD_USER_NAME": {
return { ...state, user_name: action.user_name };
}
case "rank/ADD_USER_MESSAGE": {
return { ...state, user_message: action.user_message };
}
case "rank/ADD_RANK": {
return { ...state, ranking: [...state.ranking, action.rank_info] };
}
case "rank/GET_RANK": {
return { ...state, ranking: action.rank_list };
}
default:
return state;
}
}import { createStore, combineReducers} from "redux";
import quiz from "./modules/quiz";
import rank from "./modules/rank";
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
const middlewares = [thunk];
const rootReducer = combineReducers({ quiz, rank });
const store = createStore(rootReducer);
export default store;import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./redux/configStore";
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<App />
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
// 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();import logo from './logo.svg';
import './App.css';
import React from "react";
import {Route, Switch} from "react-router-dom";
import Start from "./Start";
import Quiz from "./Quiz";
import Score from "./Score";
import Message from "./Message";
import Ranking from "./Ranking";
import { withRouter } from "react-router";
// 리덕스 스토어와 연결하기 위해 connect라는 친구를 호출할게요!
import { connect } from "react-redux";
// 이 함수는 스토어가 가진 상태값을 props로 받아오기 위한 함수예요.
const mapStateTopProps = (state) => ({
...state,
});
// 이 함수는 값을 변화시키기 위한 액션 생성 함수를 props로 받아오기 위한 함수예요.
const mapDispatchToProps = (dispatch) => ({
load: () => {
},
});
class App extends React.Component{
constructor(props){
super(props);
this.state = {
};
}
render () {
return (
<div className="App">
<Switch>
<Route path="/quiz" component={Quiz} />
<Route path="/" exact component={Start} />
<Route path="/score" component={Score} />
<Route path="/message" component={Message} />
<Route path="/ranking" component={Ranking} />
</Switch>
</div>
);
}
}
export default connect(mapStateTopProps, mapDispatchToProps)(withRouter(App));import React from "react";
import styled from "styled-components";
import { useSelector, useDispatch } from "react-redux";
import {addRank} from "./redux/modules/rank";
const Score = (props) => {
const name = useSelector((state) => state.quiz.name);
const score_texts = useSelector((state) => state.quiz.score_texts);
const answers = useSelector((state) => state.quiz.answers);
// 정답만 걸러내기
let correct = answers.filter((answer) => {
return answer;
});
// 점수 계산하기
let score = (correct.length / answers.length) * 100;
// 점수별로 텍스트를 띄워줄 준비!
let score_text = "";
// Object.keys는 딕셔너리의 키값을 배열로 만들어주는 친구예요!
Object.keys(score_texts).map((s, idx) => {
// 첫번째 텍스트 넣어주기
if (idx === 0) {
score_text = score_texts[s];
}
// 실제 점수와 기준 점수(키로 넣었던 점수) 비교해서 텍스트를 넣자!
score_text = parseInt(s) <= score ? score_texts[s] : score_text;
});
return (
<ScoreContainer>
<Text>
<span>{name}</span>
퀴즈에 <br />
대한 내 점수는?
</Text>
<MyScore>
<span>{score}</span>점<p>{score_text}</p>
</MyScore>
<Button
onClick={() => {
props.history.push("/message");
}}
outlined
>
{name}에게 한마디
</Button>
</ScoreContainer>
);
};
const ScoreContainer = styled.div`
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
padding: 16px;
box-sizing: border-box;
flex-direction: column;
justify-content: center;
align-items: center;
`;
const Text = styled.h1`
font-size: 1.5em;
margin: 0px;
line-height: 1.4;
& span {
background-color: #fef5d4;
padding: 5px 10px;
border-radius: 30px;
}
`;
const MyScore = styled.div`
& span {
border-radius: 30px;
padding: 5px 10px;
background-color: #fef5d4;
}
font-weight: 600;
font-size: 2em;
margin: 24px;
& > p {
margin: 24px 0px;
font-size: 16px;
font-weight: 400;
}
`;
const Button = styled.button`
padding: 8px 24px;
background-color: ${(props) => (props.outlined ? "#ffffff" : "#dadafc")};
border-radius: 30px;
margin: 8px;
border: 1px solid #dadafc;
width: 80vw;
`;
export default Score;import React from "react";
import styled from "styled-components";
import Score from "./Score";
import SwipeItem from "./SwipeItem";
import { useSelector, useDispatch } from "react-redux";
import {addAnswer} from "./redux/modules/quiz";
const Quiz = (props) => {
const dispatch = useDispatch();
const answers = useSelector((state) => state.quiz.answers);
const quiz = useSelector((state) => state.quiz.quiz);
const num = answers.length;
const onSwipe = (direction) => {
let _answer = direction === "left"? "O" : "X";
if(_answer === quiz[num].answer){
// 정답일 경우,
dispatch(addAnswer(true));
}else{
// 오답일 경우,
dispatch(addAnswer(false));
}
}
if (num > quiz.length -1) {
return <Score {...props}/>;
// return <div>퀴즈 끝!</div>;
}
return (
<QuizContainer>
<p>
<span>{num + 1}번 문제</span>
</p>
{quiz.map((l, idx) => {
if (num === idx) {
return <Question key={idx}>{l.question}</Question>;
}
})}
<AnswerZone>
<Answer>{"O "}</Answer>
<Answer>{" X"}</Answer>
</AnswerZone>
{quiz.map((l, idx) => {
if (idx === num) {
return <SwipeItem key={idx} onSwipe={onSwipe}/>;
}
})}
</QuizContainer>
);
};
const QuizContainer = styled.div`
margin-top: 16px;
width: 100%;
& > p > span {
padding: 8px 16px;
background-color: #fef5d4;
// border-bottom: 3px solid #ffd6aa;
border-radius: 30px;
}
`;
const Question = styled.h1`
font-size: 1.5em;
`;
const AnswerZone = styled.div`
width: 100%;
display: flex;
flex-direction: row;
min-height: 70vh;
`;
const Answer = styled.div`
width: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 100px;
font-weight: 600;
color: #dadafc77;
`;
const DragItem = styled.div`
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
& > div {
border-radius: 500px;
background-color: #ffd6aa;
}
& img {
max-width: 150px;
}
`;
export default Quiz;import React from "react";
import img from "./scc_img01.png";
import { useDispatch, useSelector } from "react-redux";
import { addUserName } from "./redux/modules/rank";
const Start = (props) => {
const dispatch = useDispatch();
const name = useSelector((state) => state.quiz.name);
const input_text = React.useRef(null);
// 컬러셋 참고: https://www.shutterstock.com/ko/blog/pastel-color-palettes-rococo-trend/
return (
<div
style={{
display: "flex",
height: "100vh",
width: "100vw",
overflow: "hidden",
padding: "16px",
boxSizing: "border-box",
}}
>
<div
className="outter"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
height: "100vh",
width: "100vw",
overflow: "hidden",
padding: "0px 10vw",
boxSizing: "border-box",
maxWidth: "400px",
margin: "0px auto",
}}
>
<img
src={img}
style={{ width: "80%", margin: "-70px 16px 48px 16px" }}
/>
<h1 style={{ fontSize: "1.5em", margin: "0px", lineHeight: "1.4" }}>
나는{" "}
<span
style={{
backgroundColor: "#fef5d4",
padding: "5px 10px",
borderRadius: "30px",
}}
>
{name}
</span>
에 대해 얼마나 알고 있을까?
</h1>
<input
ref={input_text}
type="text"
style={{
padding: "10px",
margin: "24px 0px",
border: "1px solid #dadafc",
borderRadius: "30px",
width: "100%",
// backgroundColor: "#dadafc55",
}}
placeholder="내 이름"
/>
<button
onClick={() => {
// 이름 저장
dispatch(addUserName(input_text.current.value));
// 페이지 이동
props.history.push("/quiz");
}}
style={{
padding: "8px 24px",
backgroundColor: "#dadafc",
borderRadius: "30px",
border: "#dadafc",
}}
>
시작하기
</button>
</div>
</div>
);
};
export default Start;import React from "react";
import styled from "styled-components";
import { useSelector, useDispatch } from "react-redux";
import { resetAnswer } from "./redux/modules/quiz";
const Ranking = (props) => {
const dispatch = useDispatch();
const _ranking = useSelector((state) => state.rank.ranking);
React.useEffect(() => {
// current 가 없을 때는 바로 리턴해줍니다.
if (!user_rank.current) {
return;
}
// offsetTop 속성을 이용해 스크롤을 이동하자!
window.scrollTo({
top: user_rank.current.offsetTop,
left: 0,
behavior: "smooth",
});
}, []);
// 스크롤 이동할 div의 ref를 잡아줄거예요!
const user_rank = React.useRef(null);
// Array 내장 함수 sort로 정렬하자!
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
const ranking = _ranking.sort((a, b) => {
// 높은 수가 맨 앞으로 오도록!
return b.score - a.score;
});
return (
<RankContainer>
<Topbar>
<p>
<span>{ranking.length}명</span>의 사람들 중 당신은?
</p>
</Topbar>
<RankWrap>
{ranking.map((r, idx) => {
if (r.current) {
return (
<RankItem key={idx} highlight={true} ref={user_rank}>
<RankNum>{idx + 1}등</RankNum>
<RankUser>
<p>
<b>{r.name}</b>
</p>
<p>{r.message}</p>
</RankUser>
</RankItem>
);
}
return (
<RankItem key={idx}>
<RankNum>{idx + 1}등</RankNum>
<RankUser>
<p>
<b>{r.name}</b>
</p>
<p>{r.message}</p>
</RankUser>
</RankItem>
);
})}
</RankWrap>
<Button
onClick={() => {
dispatch(resetAnswer());
window.location.href = "/";
}}
>
다시 하기
</Button>
</RankContainer>
);
};
const RankContainer = styled.div`
width: 100%;
padding-bottom: 100px;
`;
const Topbar = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100vw;
min-height: 50px;
border-bottom: 1px solid #ddd;
background-color: #fff;
& > p {
text-align: center;
}
& > p > span {
border-radius: 30px;
background-color: #fef5d4;
font-weight: 600;
padding: 4px 8px;
}
`;
const RankWrap = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-top: 58px;
`;
const RankItem = styled.div`
width: 80vw;
margin: 8px auto;
display: flex;
border-radius: 5px;
border: 1px solid #ddd;
padding: 8px 16px;
align-items: center;
background-color: ${(props) => (props.highlight ? "#ffd6aa" : "#ffffff")};
`;
const RankNum = styled.div`
text-align: center;
font-size: 2em;
font-weight: 600;
padding: 0px 16px 0px 0px;
border-right: 1px solid #ddd;
`;
const RankUser = styled.div`
padding: 8px 16px;
text-align: left;
& > p {
&:first-child > b {
border-bottom: 2px solid #212121;
}
margin: 0px 0px 8px 0px;
}
`;
const Button = styled.button`
position: fixed;
bottom: 5vh;
left: 0;
padding: 8px 24px;
background-color: ${(props) => (props.outlined ? "#ffffff" : "#dadafc")};
border-radius: 30px;
margin: 0px 10vw;
border: 1px solid #dadafc;
width: 80vw;
`;
export default Ranking;import React from "react";
import img from "./scc_img01.png";
import { useDispatch, useSelector } from "react-redux";
import {addRank} from "./redux/modules/rank";
const Message = (props) => {
const dispatch = useDispatch();
const name = useSelector((state) => state.quiz.name);
const answers = useSelector((state) => state.quiz.answers);
const user_name = useSelector((state)=>state.rank.user_name);
const input_text = React.useRef(null);
// 정답만 걸러내기
let correct = answers.filter((answer) => {
return answer;
});
// 점수 계산하기
let score = (correct.length / answers.length) * 100;
// 컬러셋 참고: https://www.shutterstock.com/ko/blog/pastel-color-palettes-rococo-trend/
return (
<div
style={{
display: "flex",
height: "100vh",
width: "100vw",
overflow: "hidden",
padding: "16px",
boxSizing: "border-box",
}}
>
<div
className="outter"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
height: "100vh",
width: "100vw",
overflow: "hidden",
padding: "0px 10vw",
boxSizing: "border-box",
maxWidth: "400px",
margin: "0px auto",
}}
>
<img
src={img}
style={{ width: "80%", margin: "-70px 16px 48px 16px" }}
/>
<h1 style={{ fontSize: "1.5em", margin: "0px", lineHeight: "1.4" }}>
<span
style={{
backgroundColor: "#fef5d4",
padding: "5px 10px",
borderRadius: "30px",
}}
>
{name}
</span>
에게 한마디
</h1>
<input
ref={input_text}
type="text"
style={{
padding: "10px",
margin: "24px 0px",
border: "1px solid #dadafc",
borderRadius: "30px",
width: "100%",
}}
placeholder="한 마디 적기"
/>
<button
onClick={() => {
let rank_info = {
score: parseInt(score),
name: user_name,
message: input_text.current.value,
current: true,
};
// 랭킹 정보 넣기
dispatch(addRank(rank_info));
// 주소 이동
props.history.push('/ranking');
}}
style={{
padding: "8px 24px",
backgroundColor: "#dadafc",
borderRadius: "30px",
border: "#dadafc",
}}
>
한마디하고 랭킹 보러 가기
</button>
</div>
</div>
);
};
export default Message;import React from "react";
import styled from "styled-components";
import { useSelector } from "react-redux";
const Progress = (props) => {
// 퀴즈 리스트 가지고 오기
const quiz_list = useSelector((state) => state.quiz.quiz);
// 유저 답 리스트 가지고 오기
const answers = useSelector((state) => state.quiz.answers);
// 답 리스트 갯수 세기
let count = answers.length;
return (
<ProgressBar>
<HighLight width={(count / quiz_list.length) * 100 + "%"} />
<Dot />
</ProgressBar>
);
};
const ProgressBar = styled.div`
width: 80%;
margin: 20px auto;
background: #eee;
// width: 100%;
height: 20px;
display: flex;
align-items: center;
border-radius: 10px;
`;
const HighLight = styled.div`
background: #df402c88;
height: 20px;
width: ${(props) => props.width};
transition: width 1s;
border-radius: 10px;
`;
const Dot = styled.div`
background: #fff;
border: 5px solid #df402c88;
box-sizing: border-box;
margin: 0px 0px 0px -10px;
width: 40px;
height: 40px;
border-radius: 20px;
`;
export default Progress;Copyright ⓒ TeamSparta All rights reserved.