[react]第3週開発ログ


この文章はスパティンコードクラブ「フロントの花、反応」の3週間の講座を聞いて書いたものだ.

SPA(Single Page Application)


:サーバにhtmlが1つしかないアプリケーション

ルート


:ブラウザのアドレスによって異なるページが表示されます.
インストール:糸add react-router-dom
  • index.jsに追加された
  • import { BrowserRouter } from "react-router-dom";
    
    ReactDOM.render(
      <BrowserRouter>
        <App />
      </BrowserRouter>,
      document.getElementById("root")
    );
  • App.js
  • import { Route } from "react-router-dom";
    import { withRouter } from "react-router";
    
    ...
    
        return (
          <div className="App">
            <div>
              // 링크 연결
              <Link to="/">Home으로 가기</Link>
              <Link to="/cat">Cat으로 가기</Link>
            </div>
            
            // 아래와 같이 Route 사용
            <Route path="/" exact component={Home} />
            <Route path="/cat" component={Cat} />
            
            <button onClick={() => {
              //push() : 페이지 이동
              this.props.history.push('/cat');
            }}>
              /cat으로 가기
            </button>
            
            <button onClick={()=>{
              // goBack(): 뒤로가기
              this.props.history.goBack();
            }}>뒤로가기
            </button>
          </div>
        );
      }
    }
    
    export default withRouter(App);
  • を正確に追加すると、ホームコンポーネントの重複データ
  • を排除できます.
  • 履歴オブジェクトを支柱として受信するには、withRouter設定
  • を使用します.

    じょうちょう


    :データを一つの場所に集めて、あちこちから出してみます.
    インストール:糸add redux react-redux
    1)状態:格納された状態値(=データ)
    2)動作:状態変化が必要な場合に発生する動作
    3)ActionCreator:アクション作成関数.アクションの作成
    4)Reducer:リードに格納されている状態を変更する関数
    5)Store:reduceを適用するために作成
    6)dispatch:アニメーションを刺激するキャラクター
  • Ridexフィーチャー
    1)storeは1個のみ使用
    2)storeの状態(データ)はactionにしか変更できません
    3)Reduserは、どのような要求があっても、同じ動作を実行する必要がある
  • .
  • ステータス管理フロー
    Ridex Storeをコンポーネントに接続します.
    コンポーネントが状態変化を必要とする場合にActionが呼び出されます.
    Reducerで新しい状態値を作成します.
    新しい状態値をStoreに保存します.
    コンポーネントは新しいステータス値を受け入れます.
  • [Toy Project] BucketList


  • ガイドを使用して遺願リストデータを削除


  • ./redux/modules/bucket.js
  • // Actions
    const LOAD = "bucket/LOAD";
    const CREATE = "bucket/CREATE";
    const DELETE = "bucket/DELETE";
    
    const initialState = {
      list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
    };
    
    // Action Creators
    export const loadBucket = (bucket) => {
      return { type: LOAD, bucket };
    };
    
    export const createBucket = (bucket) => {
      // bucket = text
      return { type: CREATE, bucket };
    };
    
    export const deleteBucket = (bucket) => {
      return { type: DELETE, bucket };
    };
    
    // Reducer
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        // do reducer stuff
        case "bucket/LOAD":
          return state;
    
        case "bucket/CREATE": {
          const new_bucket_list = [...state.list, action.bucket];
          return { list: new_bucket_list };
        }
    
        case "bucket/DELETE": {
          const bucket_list = state.list.filter((l, idx) => {
            if (idx !== action.bucket) {
              return l;
            }
          });
          return { list: bucket_list };
        }
    
        default:
          return state;
      }
    }
  • ./redux/configStore.js
  • import { createStore, combineReducers } from "redux";
    import bucket from "./modules/bucket";
    import { createBrowserHistory } from "history";
    
    export const history = createBrowserHistory();
    const rootReducer = combineReducers({ bucket });
    
    const store = createStore(rootReducer);
    
    export default store;
  • App.js
  • import React from "react";
    import styled from "styled-components";
    
    import { withRouter } from "react-router";
    import { Route, Switch } from "react-router-dom";
    
    import BucketList from "./BucketList";
    import Detail from "./Detail";
    import NotFound from "./NotFound";
    
    import { connect } from "react-redux";
    import { loadBucket, createBucket } from "./redux/modules/bucket";
    
    const mapStateToProps = (state) => {
      return { bucket_list: state.bucket.list };
    };
    
    const mapDispatchToProps = (dispatch) => {
      return {
        load: () => {
          dispatch(loadBucket());
        },
    
        create: (bucket) => {
          dispatch(createBucket(bucket));
        },
      };
    };
    
    class App extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
        };
    
        this.text = React.createRef();
      }
    
      componentDidMount() {
        console.log(this.text);
      }
    
      addBucketList = () => {
        const new_item = this.text.current.value;
        this.props.create(new_item);
      };
    
      render() {
        return (
          <div className="App">
            <Container>
              <Title>내 버킷리스트</Title>
              <Line />
              <Switch>
                <Route
                  path="/"
                  exact
                  render={(props) => (
                    <BucketList
                      bucket_list={this.props.bucket_list}
                      history={this.props.history}
                    />
                  )}
                />
                <Route path="/detail/:index" component={Detail} />
                <Route
                  render={(props) => <NotFound history={this.props.history} />}
                />
              </Switch>
            </Container>
            <Input>
              <input type="text" ref={this.text} />
              <button onClick={this.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: 80vh;
      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 connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
  • Detail.js
  • import React from "react";
    import { useSelector, useDispatch } from "react-redux";
    
    import { deleteBucket } from "./redux/modules/bucket";
    
    const Detail = (props) => {
      const dispatch = useDispatch();
    
      const bucket_list = useSelector((state) => state.bucket.list);
      console.log(bucket_list, props);
      const bucket_index = parseInt(props.match.params.index);
    
      return (
        <div>
          <h1>{bucket_list[bucket_index]}</h1>
          <button
            onClick={() => {
              dispatch(deleteBucket(bucket_index));
              props.history.goBack();
            }}
          >
            삭제하기
          </button>
        </div>
      );
    };
    
    export default Detail;
  • BucketList.js
  • import React from "react";
    import styled from "styled-components";
    
    import { useDispatch, useSelector } from "react-redux";
    
    const BucketList = (props) => {
      const bucket_list = useSelector((state) => state.bucket.list);
    
      console.log(bucket_list);
    
      return (
        <ListStyle>
          {bucket_list.map((list, index) => {
            return (
              <ItemStyle
                className="list_item"
                key={index}
                onClick={() => {
                  props.history.push("/detail/" + index);
                }}
              >
                {list}
              </ItemStyle>
            );
          })}
        </ListStyle>
      );
    };
    
    const ListStyle = styled.div`
      display: flex;
      flex-direction: column;
      height: 100%;
      overflow-x: hidden;
      overflow-y: auto;
    `;
    
    const ItemStyle = styled.div`
      padding: 16px;
      margin: 8px;
      background-color: aliceblue;
    `;
    
    export default BucketList;
  • Quiz.js
  • import React from "react";
    import styled from "styled-components";
    import img from "./ponyo.jpg";
    import TinderCard from "react-tinder-card";
    import SwipeItem from "./SwiptItem";
    import Score from "./Score";
    
    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 (
                <DragItem key={idx}>
                  <TinderCard
                    onSwipe={onSwipe}
                    onCardLeftScreen={onSwipe}
                    onCardRightScreen={onSwipe}
                    preventSwipe={["up", "down"]}
                  >
                    <img src={img} />
                  </TinderCard>
                </DragItem>
              );
              //   <SwipeItem key={idx} onSwipe={onSwipe} />;
            }
          })}
        </QuizContainer>
      );
    };
    
    const QuizContainer = styled.div`
      text-align: center;
      margin-top: 130px;
      & > p > span {
        padding: 8px 16px;
        background-color: #ffe08c;
        border-radius: 30px;
        font-weight: bold;
      }
    `;
    
    const Question = styled.h1`
      font-size: 1.5em;
    `;
    
    const AnswerZone = styled.div`
      width: 100vw;
      height: 100vh;
      display: flex;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 1;
    `;
    
    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;
      z-index: 10;
    
      & img {
        max-width: 130px;
      }
    `;
    
    const Finish = styled.p`
      text-align: center;
      margin-top: 300px;
      font-weight: 600;
      font-size: 30px;
    `;
    
    export default Quiz;
  • 結果画面
    -希望リスト項目を追加した後、詳細ページ
  • の詳細ページで「削除」をクリックし、戻りながらリストから削除します.
  • [HW] FriendTest Project


  • App.js
  • import React from "react";
    import "./App.css";
    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";
    import { connect } from "react-redux";
    
    const mapStateToProps = (state) => ({
      ...state,
    });
    
    const mapDispatchToProps = (dispatch) => ({
      load: () => {},
    });
    
    class App extends React.Component {
      constructor(props) {
        super(props);
    
        this.state = {};
      }
    
      render() {
        return (
          <div className="App">
            <Switch>
              <Route path="/" exact component={Start} />
              <Route path="/quiz" component={Quiz} />
              <Route path="/score" component={Score} />
              <Route path="/message" component={Message} />
              <Route path="/ranking" component={Ranking} />
            </Switch>
          </div>
        );
      }
    }
    
    export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
  • Start.js
  • import React from "react";
    import img from "./ponyo.jpg";
    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);
    
      return (
        <div className="container">
          <div className="outter">
            <img className="scc-img" src={img} />
            <h1>
              나는 <span>{name}</span>에 대해 얼마나 알고 있을까?
            </h1>
            <input
              ref={input_text}
              className="text-box"
              type="text"
              placeholder="내 이름"
            />
            <button
              className="button"
              onClick={() => {
                dispatch(addUserName(input_text.current.value));
                props.history.push("/quiz");
              }}
            >
              시작하기
            </button>
          </div>
        </div>
      );
    };
    
    export default Start;
  • Score.js
  • 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(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: 20px;
      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.7;
      text-align: center;
      & span {
        background-color: #ffe08c;
        padding: 5px 10px;
        border-radius: 30px;
      }
    `;
    
    const MyScore = styled.div`
      & span {
        border-radius: 25px;
        padding: 5px 10px;
        background-color: #ffe08c;
      }
      font-weight: 600;
      font-size: 2em;
      margin: 25px;
      text-align: center;
    
      & > p {
        margin: 20px 0px;
        font-size: 18px;
        font-weight: 550;
      }
    `;
    const Button = styled.button`
      color: white;
      padding: 10px 20px;
      background-color: #6799ff;
      border-radius: 30px;
      margin: 10px;
      border: 1px solid #b2ccff;
      width: 70vw;
    `;
    
    export default Score;
  • Ranking.js
  • 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);
    
      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) => {
              return (
                <RankItem key={idx} highlight={r.current ? true : false}>
                  <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;
  • Message.js
  • import React from "react";
    import img from "./ponyo.jpg";
    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;
    
      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;
  • ./redux/modules/quiz.js
  • //Actions
    const GET_QUIZ = "quiz/GET_QUIZ";
    const ADD_ANSWER = "quiz/ADD_ANSWER";
    const RESET_ANSWER = "quiz/RESET_ANSWER";
    
    const initialState = {
      name: "포뇨",
      score_texts: {
        60: "우린 친구! 앞으로도 더 친하게 지내요!",
        80: "우와! 우리는 엄청 가까운 사이!",
        100: "우린 둘도 없는 단짝! :)",
      },
    
      answers: [],
    
      quiz: [
        { question: "포뇨는 5살이다.", answer: "O" },
        { question: "포뇨는 주황색 머리다.", answer: "O" },
        { question: "포뇨는 물을 좋아한다.", answer: "O" },
        { question: "포뇨는 라면을 좋아한다.", answer: "O" },
      ],
    };
    
    export const getQuiz = (quiz_list) => {
      return { type: GET_QUIZ, quiz_list };
    };
    
    export const addAnswer = (answer) => {
      return { type: ADD_ANSWER, answer };
    };
    
    export const resetAnswer = () => {
      return { type: RESET_ANSWER };
    };
    
    //Reducer
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        case "quiz/GET_QUIZ": {
          return { ...state, quiz: action.quiz_list };
        }
    
        case "quiz/ADD_ANSWER": {
          return { ...state, answers: [...state.answers, action.answer] };
        }
    
        case "quiz/RESET_ANSWER": {
          return { ...state, answers: [] };
        }
    
        default:
          return state;
      }
    }
  • ./redux/modules/rank.js
  • // 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: "안녕 포뇨!" }],
    };
    
    export const addUserName = (user_name) => {
      return { type: ADD_USER_NAME, user_name };
    };
    
    export const addRank = (rank_info) => {
      return { type: ADD_RANK, rank_info };
    };
    
    export const addUserMessage = (user_message) => {
      return { type: ADD_USER_MESSAGE, user_message };
    };
    
    export const getRank = (rank_list) => {
      return { type: GET_RANK, rank_list };
    };
    
    //Reducer
    export default function reducer(state = initialState, action = {}) {
      switch (action.type) {
        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;
      }
    }
  • 結果画面

  • ❗詳細スタイルはまだきれいに適用されていません.もっとスタイルを重視して完成する必要があります.
    「100 Quiz」画面でswifeを1回行う場合は、2回発生したエラーを修正する必要があります.