한 입 크기로 잘라먹는 리액트 7강

이안이다·2023년 6월 5일
0

React

목록 보기
6/7

자 이제 7강 리액트다. 저번 시간까지 일기장 만들기를 맛보기로 만들었다면 이제 실전 프로젝트를 시작한다고 한다. 재밌겠다 !!! 레쓰고


7-1



이렇게 세 개의 페이지를 제공하는 웹서비스를 만들거라고 한다.

가장 먼저 알아야 할 개념은 바로 페이지 라우팅 !!

PAGE ROUTING

먼저 라우팅이란? 어떤 네트워크 내에서 통신 데이터를 보낼 경로를 선택하는 일련의 과정을 말한다. 지하철역으로 비유하여 이해하면 편하다. 먼 목적지로 데이터가 지하철을 타고 가려고 할 때 수차례 환승을 통해서 이동해야 한다. 교통량과 혼잡도를 고려하여 최적의 환승경로를 네비가 설정해주듯, 라우터는 Date들이 어떤 경로로 이동해야 가장 빠른지 정해주는 역할을 한다. 거기에 ing를 붙여서 경로를 정해주는 행위 자체와 그런 과정들을 다 포함하여 일컫는 말을 라우팅이라고 하는 거다 !!

자 라우팅은 알겠는데 그럼 페이지 라우팅은 뭘까?

내가 /home이라는 경로를 서버에 보내면 서버는 그 경로를 확인하고 Home.html을 다시 브라우저로 쏴준다. 그래서 내가 그 페이지를 볼 수 있는 거다. 즉, /home이라는 Request(요청)을 보내면 서버가 그에 적합한 html파일을 보내주는 것을 페이지 라우팅이라고 하는 것이다. 개념은 일단 어렵지 않은 것 같다 ㅎㅎ


여러 페이지가 쫙쫙 되는 걸 이렇게 부른다고 한다. 중요한건 이런 게 MPA방식으로 구현된 것!

근데 내가 지난 시간에 만들었던 일기장은 Single Page Application 즉 SPL이다. 페이지가 하나밖에 없으니까 서버는 나에게 다른 페이지를 줄 수가 없다. 근데 그건 나 때문이 아니다. 리액트는 SPL방식만 지원한다.

그렇다고 리액트는 그럼 여러개의 페이지를 업데이트하면서 다루는 게 불가능하냐!? 그거슨 아니다.

서버가 적절한 페이지를 브라우저한테 리스폰해주는 식이 아니라, 리액트가 그냥 브라우저 안에서 직접 index.html을 페이지 업데이트 해주는 식으로 가능하다. 서버 대기시간도 필요 없어지고 그냥 바로 슈슉 하고 컴포넌트가 교체되듯 페이지가 교체된다.

간단하게 정리해보자 !!

리액트는 SPA 방식을 지키면서 CSR 방법으로 페이지를 렌더링해준다 !!!


7-2 페이지 라우팅 1, 2

자 본격적으로 만들어보자

먼저 새로운 프로젝트를 만들어주자. 어떻게 만드는지 기억 안나서 이전 강의 찾아봤다ㅎㅎ

시작부터 에러가 뜬다. 날 미치게 한다.
대문자를 포함하면 안되는거였구나 ...?


다시 열심히 생성해주고 ...

만들어졌으면 메인 폴더 안에 메인 폴더랑 똑같은 이름 폴더 생기고 그 안에 필요한 파일들이 들어가 있으니까 전부 잘라내서 하나 상위폴더로 와서 원래 메인폴더에다가 붙여넣기 해주는 거 잊지 말장

강사는 필요없는 사진들 파일들 또 지우던데 괜히 따라하다가 에러뜰까봐 무서워서 난 아무것도 안 지우고 그냥 그대로 하련다.

npm install react-router-dom@6
그리고
npm start

자 시작해보자 ~!

자 이렇게 11번 15번줄 처럼 BrowserRouter이라는 태그로 감싸주면 저기 안에 쌓인 애들은 모두 localhost:3000에 브라우저 라우팅을 받을 수 있게 되는거임 !
그리고 4~7줄 처럼 새로 만들 파일들 다 import해준다.
근데 15번줄을 보면 알겠지만 이렇게 하면 Home 페이지만 렌더링 된다. 따라서 나머지 세 개도 같은 형식으로 Route로 넣어준다.

14번줄 Routes태그 안에 묶인 애들은 계속 새로 바뀔 페이지들에 대한 요소를 넣는거고 h2처럼 쟤 밖에 있는 애들은 새로 바뀌지 않을 고정될 애들임 !!


새로고침 하지 않고 요소 누르면 딱 그녀석들만 착착 렌더링 됨.
그냥 html에서 a태그 넣어서 이동시키면 SPL방법이 아닌거니까 리액트의 장점을 사용하지 못 하는 거기 땜시 그렇게 하지 말자.


이거 유용하다니까 바로 캡쳐.

import { useParams } from "react-router-dom";
const { id } = useParams();
//이렇게 두 개를 써서 사용자 정의 후크 -> 커스텀 후크 사용 가능


이번에는 쿼리 ~!

리액트 라우터에서는 쿼리 스트링을 어떻게 다루는지 이해하면 되는 부분이다.

localhost:3000/edit?id=10&mode=dark

이렇게 url에다가 바로 쿼리로 쓸 수 있는데 물음표 뒤로는 페이지 라우팅하는 경로에 영향을 주지 않는다.

import { useNavigate, useSearchParams } from "react-router-dom";

const Edit = () => {
  const [searchParams, setSearchParams] = useSearchParams();
  const navigate = useNavigate();

  return (
    <div>
      <h1>Edit</h1>
      <p>여기는 ~~~ 수정페이지 ~~~</p>
      <button onClick={() => setSearchParams({ who: "김이안" })}>
        QS 바꾸기
      </button>

      <button onClick={() => navigate("/home")}>HOME으로 가기</button>
      <button onClick={() => navigate(-1)}>뒤로가기</button>
    </div>
  );
};

export default Edit;


이렇게 쿼리스트링을 이용해서 새로고침 없이 바로 렌더링하는 방법까지 알아봤다. 이제 진짜 프로젝트 기초공사 시작하러 가보장~


7-4, 5 프로젝트 기초 공사

먼저 글씨체 !!
google font web에 들어가서 원하는 거 고르고 사이드 바 열면

요렇게 import를 선택하면 css에 넣어야할 코드가 나옴.

@import url('https://fonts.googleapis.com/css2?family=Nanum+Gothic&family=Open+Sans:wght@400;600&family=Yeon+Sung&display=swap');
.App {
  padding: 20px;

  font-family: 'Nanum Gothic', sans-serif;
  font-family: 'Yeon Sung', cursive;
}

이렇게 해주면 된다.

글씨가 매우 이뿌다 ...

font-family 속성이 여러개가 있으면 가장 마지막에 적혀있는 놈이 스타일링 된다 !!!

사실 아는거다 ㅋ

글씨체 다 했으니 이번에는 레이아웃 !!!
display: flex 써서 슥슥삭삭 만드는데 css는 이미 마스터라 새로운 내용 하나도 없었으니 결과물만 넣겠음.

자 이번에는 이미지 세팅 !!!

<img src={process.env.PUBLIC_URL + "/assets/emotion1.png"} />

App.js에서 BrouserRouter 태그 안에 이 코드를 넣을건데 이건 처음보는 거다. 이건 무슨 역할을 하냐면

화살표처럼 내가 어느 위치에 있든지간에 저 public에 대한 경로를 바로 쓸 수 있게 해주는 명령어로 이해하면 된다.

자 마지막으로 공통 컴포넌트 세팅 !!!


이 버튼들을 컴포넌트로 구현할거다.

일단 CSS로 열심히 꾸미고 있는

왜 내 버튼들은 이모양인걸까??...

혼자 열 내면서 한참 고민했는데 MyButton을 Mybutton이라고 클래스명에 대소문자 오타가 원인이었다. 진짜 갈아엎고싶다 진짜 화난다 진짜 열불이 난다 정말


이번엔 또 색이 적용이 안 된다.

.Mybutton_default {
  background-color: #ececec;
  color: black;
}

.Mybutton_positive {
  background-color: #64c964;
  color:white;
}

.Mybutton_negative {
  background-color:#fd565f;
  color: white;
}

하하하하하하 열받네 여기서도 button이라고 해놨었다,,

자... 계속 진도 나가자.

const btnType = ["positive", "negative"].includes(type) ? type : "default";

신기한 문법인데, type이 []속에 있는지 없는지 없다면 type을 디폴트로 고정시키겠다는 명령이다.

예외처리까지 끝났으니 이제 헤드 컴포넌트까지 만들어보자.


3개의 prop에 따라 각각 다르게 동작하게 만들어야 한다.
왼쪽 자식을 leftChild, 헤드 텍스트는 headText, 오른쪽은 rightChild로 잡으면 될 것 같다.


App이라는 임시 문구로 일단 헤더텍스트 위치 잡았고 flexbox로 레이아웃들을 구현했다.

        <MyHeader
          headText={"App"}
          leftChild={
            <MyButton text={"왼쪽 버튼"} onClick={() => alert("왼쪽 클릭")} />
          }
          rightChild={
            <MyButton
              text={"오른쪽 버튼"}
              onClick={() => alert("오른쪽 클릭")}
            />
          }
        />

이거 되게 유용하다. prop을 두 번 보낼 거 한 번에 보내게 해준다. 이해하기도 쉽고 직관적이다. 왼쪽, 오른쪽 헤더에 버튼까지 구현 끝냈다 !! 이제 두번 째 기초 공사 하러 가보장.

강사가 갑자기 지금까지 힘들게 따라 만들었던 버튼들과 헤더들, 다 지워버리란다. 제정신이 맞는가 이럴거면 왜 따라하라고 시킨걸까 진짜,,,

data랑 date를 자꾸 헷갈려서 지금 한참 고생했다.

import React, { useReducer, useRef } from "react";

import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";

import Home from "./pages/Home";
import New from "./pages/New";
import Edit from "./pages/Edit";
import Diary from "./pages/Diary";

const reducer = (state, action) => {
  let newState = [];
  switch (action.type) {
    case "INIT": {
      return action.data;
    }
    case "CREATE": {
      const newItem = {
        ...action.data,
      };
      newState = [newItem, ...state];
      break;
    }
    case "REMOVE": {
      newState = state.filter((it) => it.id !== action.targetId);
      break;
    }
    case "EDIT": {
      newState = state.map((it) =>
        it.id === action.data.id ? { ...action.data } : it
      );
      break;
    }
    default:
      return state;
  }
  return newState;
};

export const DiaryStateContext = React.createContext();
export const DiaryDispatchContext = React.createContext();

function App() {
  const [data, dispatch] = useReducer(reducer, []);

  const dataId = useRef(0);
  //CREATE
  const onCreate = (date, content, emotion) => {
    dispatch({
      type: "create",
      data: {
        id: dataId.current,
        date: new Date(date).getTime(),
        content,
        emotion,
      },
    });
    dataId.current += 1;
  };
  //REMOVE
  const onRemove = (targetId) => {
    dispatch({ type: "REMOVE", targetId });
  };
  //EDIT
  const onEdit = (targetId, date, content, emotion) => {
    dispatch({
      type: "EDIT",
      data: {
        id: targetId,
        date: new Date(date).getTime(),
        content,
        emotion,
      },
    });
  };
  return (
    <DiaryStateContext.Provider value={data}>
      <DiaryDispatchContext.Provider
        value={{
          onCreate,
          onEdit,
          onRemove,
        }}
      >
        <BrowserRouter>
          <div className="App">
            <Routes>
              <Route path="/home" element={<Home />} />
              <Route path="/new" element={<New />} />
              <Route path="/edit" element={<Edit />} />
              <Route path="/diary" element={<Diary />} />
            </Routes>
          </div>
        </BrowserRouter>
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>
  );
}

export default App;

App.js를 기초공사 2까지 완성한 단계다. 일단 생각보다 낯선 문법들이 많았던 것 같고 솔직히 하나하나 제대로 이해 못하겠다. 변수에는 값을 딱 넣어주는 게 익숙한데 prop을 위해 다른 무언가를 호출하는 녀석을 넣고 그 녀석은 또 누군가를 부르고 있는 이 복잡한 거 마음에 안 든다. 그래도 직접 영상 보면서 하나하나 따라 치니까 어렴풋이나마 이해는 되는 것 같은데 오래 못가 까먹을 것 같다 ...


코드는 이렇게 긴데 웹에는 꼴랑 이거만 뜨는거 너무 허탈하다.


자 이번에는 페이지(홈)를 구현할 거다 ~!!
무려 한시간짜리 강의라 긴장되지만.... 천천히 천천히 ...


제발 이러지 마 ...
일단 어떻게 저떻게 완성된 오늘의 코드. 날아가기 전에 백업 겸 다 올려놔야지. 오류 고칠 때까지 내맘대로 막 뜯어고칠거임.

App.js

import "./App.css";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import Home from "./pages/Home";
import New from "./pages/New";
import Edit from "./pages/Edit";
import Diary from "./pages/Diary";
import React, { useReducer, useRef } from "react";

const reducer = (state, action) => {
  let newState = [];
  switch (action.type) {
    case "INIT": {
      return action.date;
    }
    case "CREATE": {
      const newItem = {
        ...action.data,
      };
      newState = [newItem, ...state];
      break;
    }
    case "REMOVE": {
      newState = state.filter((it) => it.id !== action.targetId);
      break;
    }
    case "EDIT": {
      newState = state.map((it) =>
        it.id === action.data.id ? { ...action.data } : it
      );
      break;
    }
    default:
      return state;
  }
  return newState;
};

export const DiaryStateContent = React.createContext();
export const DiaryDispatchContext = React.createContext();

const dummyData = [
  {
    id: 1,
    emotion: 1,
    content: "일기 1번",
    date: 1685806000745,
  },
  {
    id: 2,
    emotion: 1,
    content: "일기 2번",
    date: 1685806000750,
  },
  {
    id: 3,
    emotion: 1,
    content: "일기 3번",
    date: 1685806000759,
  },
];

function App() {
  const [data, dispatch] = useReducer(reducer, dummyData);
  const dataId = useRef(0);
  //Create
  const onCreate = (date, content, emotion) => {
    dispatch({
      type: "CREATE",
      data: {
        id: dataId.current,
        date: new Date(date).getTime(),
        content,
        emotion,
      },
    });
    dataId.current += 1;
  };

  const onRemove = (targetId) => {
    dispatch({ type: "REMOVE", targetId });
  };

  const onEdit = (targetId, date, content, emotion) => {
    dispatch({
      type: "EDIT",
      data: {
        id: targetId,
        date: new Date(date).getTime(),
        content,
        emotion,
      },
    });
  };

  return (
    <DiaryStateContent.Provider value={data}>
      <DiaryDispatchContext.Provider value={{ onCreate, onEdit, onRemove }}>
        <BrowserRouter>
          <div className="App">
            <Routes>
              <Route path="/" element={<Home />} />
              <Route path="/new" element={<New />} />
              <Route path="/edit" element={<Edit />} />
              <Route path="/diary/:id" element={<Diary />} />
            </Routes>
          </div>
        </BrowserRouter>
      </DiaryDispatchContext.Provider>
    </DiaryStateContent.Provider>
  );
}

export default App;

Home.js

import { useContext, useEffect, useState } from "react";
import { DiaryStateContent } from "../App";
import Myheader from "./../components/MyHeader";
import MyButton from "./../components/MyButton";
import DiaryList from "./../components/DiaryList";
import React from "react";

const Home = () => {
  const diaryList = useContext(DiaryStateContent);

  const [data, setData] = useState([]);
  const [curDate, setCurDate] = useState(new Date());
  const headText = `${curDate.getFullYear()}${curDate.getMonth() + 1}`;

  useEffect(() => {
    if (diaryList.length >= 1) {
      const firstDay = new Date(
        curDate.getFullYear(),
        curDate.getMonth(),
        1
      ).getTime();

      const lastDay = new Date(
        curDate.getFullYear(),
        curDate.getMonth() + 1,
        0
      ).getTime();

      setData(
        diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay)
      );
    }
  }, [diaryList, curDate]);

  const increaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate())
    );
  };
  const decreaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate())
    );
  };

  return (
    <div>
      <Myheader
        headText={headText}
        leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
        rightChild={<MyButton text={">"} onClick={increaseMonth} />}
      />
      <DiaryList diaryList={data} />
    </div>
  );
};

export default Home;

DiaryList.js

import { useContext, useEffect, useState } from "react";
import Myheader from "./../components/MyHeader";
import MyButton from "./../components/MyButton";
import DiaryList from "./../components/DiaryList";
import React from "react";
import { DiaryStateContent } from "../App";

const Home = () => {
  const diaryList = useContext(DiaryStateContent);

  const [data, setData] = useState([]);
  const [curDate, setCurDate] = useState(new Date());
  const headText = `${curDate.getFullYear()}${curDate.getMonth() + 1}`;

  useEffect(() => {
    if (diaryList.length >= 1) {
      const firstDay = new Date(
        curDate.getFullYear(),
        curDate.getMonth(),
        1
      ).getTime();

      const lastDay = new Date(
        curDate.getFullYear(),
        curDate.getMonth() + 1,
        0
      ).getTime();

      setData(
        diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay)
      );
    }
  }, [diaryList, curDate]);

  const increaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() + 1, curDate.getDate())
    );
  };
  const decreaseMonth = () => {
    setCurDate(
      new Date(curDate.getFullYear(), curDate.getMonth() - 1, curDate.getDate())
    );
  };

  return (
    <div>
      <Myheader
        headText={headText}
        leftChild={<MyButton text={"<"} onClick={decreaseMonth} />}
        rightChild={<MyButton text={">"} onClick={increaseMonth} />}
      />
      <DiaryList diaryList={data} />
    </div>
  );
};

export default Home;

DiaryItem.js

import MyButton from "./MyButton";
import { useNavigate } from "react-router-dom";

const DiaryItem = ({ id, emotion, content, date }) => {
  const navigate = useNavigate();
  const strDate = new Date(parseInt(date)).toLocaleDateString();

  const goDetail = () => {
    navigate(`/diary/${id}`);
  };
  const goEdit = () => {
    navigate(`/edit/${id}`);
  };
  return (
    <div className="DiaryItem">
      <div
        onClick={goDetail}
        className={[
          "emotion_img_wrapper",
          `emotion_img_wrapper_${emotion}`,
        ].join(" ")}
      >
        <img src={process.env.PUBLIC_URL + `assets/emotion${emotion}.png`} />
      </div>
      <div onClick={goDetail} className="info_wrapper">
        <div className="diary_date">{strDate}</div>
        <div className="diary_content_preview">{content.slice(0, 25)}</div>
      </div>
      <div className="btn_wrapper">
        <MyButton onClick={goEdit} text={"수정하기"} />
      </div>
    </div>
  );
};

처음에 경로에 이슈가 계속 발생해서 헤맸는데 이걸 잘 이해해야한다.

내가 지금 렌더링하려는 그 파일이 들어있는 폴더의 위치 파악부터 똑바로 해서 루트로 올라갈지 하나 상위폴더로 갈 지 정확히 적기 그리고 경로명에 오타내지 말기 ...........

profile
경제와 개발을 곁들인 기획자..!

3개의 댓글

comment-user-thumbnail
2023년 6월 5일

글만 읽어도 이안 슨배님 목소리가 들리는듯 하네요! ㅎㅎㅎㅎ
읽다보니 역시 오류의 99.9%는 오타가 이유인게 느껴집니다.. 저도 그랬거든요,,ㅜㅜ
역시 선배님답게 정리도 잘되있고 이해도 편했습니다!
그리고 역시 포스팅에 연륜이 묻어나오는 듯 하네요!ㅋㅋㅋ
제 글에도 댓글 한번 부탁드립니다~~!

답글 달기
comment-user-thumbnail
2023년 6월 6일

옹 ㅋ 결국 다 했네 칭찬합니당 ~~

답글 달기

정리 엄청 잘 해놓으셨네요!
저도 한 시간 동안 오타지옥 경험하고 정신 나갈 뻔 했읍니다
수고 많으셨어요!

답글 달기