React

 

 

인프런 : https://www.inflearn.com/course/한입-리액트

 

강의 진행 중 사용된 소스코드 입니다.

https://codesandbox.io/s/chapt-1-otxc8

 

소스 :  https://github.com/braverokmc79/react-with-one-bite

https://github.dev/braverokmc79/react-simple-diary

 

https://github.dev/braverokmc79/react-emotion-diary

 

 

 

들어가며
1.강의 및 강사 소개
2. React 소개
3.오픈채팅방 입장 방법 및 비밀번호

 

JavaScript 기본
4.Hello World
5.변수와 상수
6.자료형과 형 변환
7.연산자
8.조건문
9.함수
10.함수표현식 & 화살표 함수
11.콜백함수
12.객체
13.배열
14.반복문
15.배열 내장 함수


JavaScript 응용
16.Truthy & Falsy
17.삼항 연산자
18.단락회로 평가
19.조건문 업그레이드
20.비 구조화 할당
21.Spread 연산자
22.동기 & 비동기
23.Promise - 콜백 지옥에서 탈출하기
24.async & await - 직관적인 비 동기 처리 코드 작성하기
25.API 호출하기


Node.js 기초
26.Node.js란?
27.Node.js & VsCode 설치하기
28.Node.js Hello World & Common JS
29.Node.js 패키지 생성 및 외부 패키지 사용하기


React.js 기초
30.Why React?
31.Create React App
32.JSX
33.State
34.Props

 

React 기본 - 간단한 일기장 프로젝트
35.프로젝트 소개
36.React에서 사용자 입력 처리하기
37.React에서 DOM 조작하기 - useRef
38.React에서 배열 사용하기 1 - 리스트 렌더링 (조회)
39.React에서 배열 사용하기 2 - 데이터 추가하기
40.React에서 배열 사용하기 3 - 데이터 삭제하기
41.React에서 배열 사용하기 4 - 데이터 수정하기
42.React Lifecycle 제어하기 - useEffect
43.React에서 API 호출하기
44.React developer tools
45.최적화 1 - useMemo
46.최적화 2 - React.memo
47.최적화 3 - useCallback
48.최적화 4 - 최적화 완성
49.복잡한 상태 관리 로직 분리하기 - useReducer
50.컴포넌트 트리에 데이터 공급하기 - Context

 

 

React 실전 프로젝트 - 감정 일기장 만들기
51.프로젝트 완성 예시
52.페이지 라우팅 0 - React SPA & CSR
53.페이지 라우팅 1 - React Router 기본
54.페이지 라우팅 2 - React Router 응용
55.프로젝트 기초 공사 1
56.프로젝트 기초 공사 2
57.페이지 구현 - 홈 (/)
58.페이지 구현 - 일기 쓰기 (/new)
59.페이지 구현 - 일기 수정 (/edit)
60.페이지 구현 - 일기 상세 (/diary)
61.(서브 챕터) 흔히 발생하는 버그 수정 하기
62.LocalStorage를 일기 데이터베이스로 사용하기
63.프로젝트 최적화
64.배포 준비 & 프로젝트 빌드하기
65.Firebase로 프로젝트 배포하기
66.Open Graph 설정하기


 

 

 

React 실전 프로젝트 - 감정 일기장 만들기

 

51.프로젝트 완성 예시

 

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103539?tab=curriculum

 

데모사이트는 아래의 링크에서 확인하실 수 있습니다.

https://emotion-diary-winterlood.web.app/

 

 

 

 

 

52.페이지 라우팅 0 - React SPA & CSR

 

강의 : 

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103540?tab=curriculum

 

 

 

 

1. SPA란?

SPA이란, Single Page Application의 약자로 단일 페이지 애플리케이션을 의미한다.

 

2. MPA란?

MPA란, Multi Page Applicaton의 약자로 페이지가 요청될 때마다, 서버로부터 전체 페이지를 받아오고, 이후에 데이터의 수정이나 조회가 필요할 때 완전히 다른 페이지로 이동하는 것을 말한다.

MPA는 SPA와는 다르게 SEO 관점에서 유리하며, 검색엔진이 크롤링할 때 유리하다. 하지만, 아주 작은 변화에도 전체 페이지를 렌더링하기 때문에 성능이 효율적이지 못하다.

 

3. SSR이란?

SSR은 Server Side Rendering의 약자로 서버 사이드 렌더링이라고 부르며, 서버에서 사용자에게 보여줄 페이지를 정적인 페이지(HTML)로 미리 렌더링하여 사용자에게 보여주는 것을 말한다. 한마디로, 서버 측에서 렌더링 준비가 다 끝난 채로 사용자에게 전달되는 것이다.

 

4. CSR이란?

CSR은 Client Side Rendering의 약자로 클라이언트 사이드 렌더링이라고 부르며, 사용자가 처음 접속할 때, 서버 측에서 HTML과 자바스크립트를 사용자에게 보내주면 렌더링을 시작한다.

 

+ SEO란?

SEO는 Search Engine Optimization의 약자로서 웹페이지 검색엔진이 자료를 수집하고 순위를 매기는 방식에 맞추어 웹페이지를 구성하여 검색 결과의 상위에 나올 수 있도록 하는 작업을 의미한다.

 

참고로 React는 SPA를 지원하며, CSR 방식으로 동작한다. 

 

 

 

 

 

 

 

53.페이지 라우팅 1 - React Router 기본

 

강의  :  https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103541?tab=curriculum

 

 

react-emotion-diary 디렉토리 만들고

 

$ npx create-react-app .


 

 

참조 :

1 ) https://macaronics.net/index.php/m04/react/view/1909

2)  React) React Router V6를 한번 써보기

 

설치하기 및 라우터 시작하기

npm install react-router-dom@6

 

일단 해당 프로젝트로 들어가 위 커맨드를 통해 라우터를 설치해주세요.

그리고 Index.js(도입부)에서 아래와 같이 라우터를 사용하겠다고 알려줍니다.

BrowerRouter는 Router를 사용하겠다는 전체적인 Wrap과 같은 겁니다.

이때 중요한것이 Import에 'react-router-dom'을 해주셔야합니다.

 

 

 

App.js

import './App.css';
import { BrowserRouter, Routes, Route } 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 RouteTest from './components/RouteTest';


function App() {
  return (
    <BrowserRouter>
      <div className="App">
        <h2>App.js</h2>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/new" element={<New />} />
          <Route path="/edit" element={<Edit />} />
          <Route path="/diary" element={<Diary />} />
        </Routes>

        <RouteTest />
      </div>
    </BrowserRouter >
  );
}

export default App;

 

pages/Home.js

import React from 'react';

const Home = () => {
    return (
        <div>
            <h1>Home</h1>
            <p>이곳은 홈 입니다.</p>
        </div>
    );
};

export default React.memo(Home);

 

components/RouteTest.js

import { Link } from 'react-router-dom';


const RouteTest = () => {
    return (
        <div>
            <Link to={'/'} >HOME</Link>
            <br /> <br />
            <Link to={"/diary"}>DIARY</Link>
            <br /> <br />
            <Link to={"/new"}>NEW</Link>
            <br /> <br />
            <Link to={"/edit"}>EDIT</Link>
        </div>
    );
};

export default RouteTest;

 

 

 

 

 

 

 

 

 

54.페이지 라우팅 2 - React Router 응용

 

강의 : https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103542?tab=curriculum

 

 

 

1.useParams

useParams후크는 에 의해 일치된 현재 URL에서 동적 매개변수의 키/값 쌍의 개체를 반환 합니다

 <Route path>. 자식 경로는 부모 경로에서 모든 매개변수를 상속합니다.

import * as React from 'react';
import { Routes, Route, useParams } from 'react-router-dom';

function ProfilePage() {
  // Get the userId param from the URL.
  let { userId } = useParams();
  // ...
}

function App() {
  return (
    <Routes>
      <Route path="users">
        <Route path=":userId" element={<ProfilePage />} />
        <Route path="me" element={...} />
      </Route>
    </Routes>
  );
}

 

 

 

2. useSearchParams  ★★★ 

 

useSearchParams후크는 현재 위치에 대한 URL의 쿼리 문자열을 읽고 수정하는 데 사용됩니다 

. React의 useState후크useSearchParams 와 마찬가지로 현재 위치의 검색 매개변수 와 이를 업데이트하는 데 사용할 수 있는 함수라는 두 가지 값의 배열을 반환합니다 . React의 useStatehook 과 마찬가지로 기능 업데이트setSearchParams 도 지원 합니다 .

 따라서 업데이트된 버전을 가져와 반환하는 함수를 제공할 수 있습니다.

searchParams

 

import * as React from "react";
import { useSearchParams } from "react-router-dom";

function App() {
  let [searchParams, setSearchParams] = useSearchParams();

  function handleSubmit(event) {
    event.preventDefault();
    // The serialize function here would be responsible for
    // creating an object of { key: value } pairs from the
    // fields in the form that make up the query.
    let params = serializeFormQuery(event.target);
    setSearchParams(params);
  }

  return (
    <div>
      <form onSubmit={handleSubmit}>{/* ... */}</form>
    </div>
  );
}

 

Edit.js

http://localhost:3000/edit?mode=dark&id=1990

import * as React from "react";
import { useSearchParams } from 'react-router-dom';


const Edit = () => {

    const [searchParams, setSearchParams] = useSearchParams();

    const id = searchParams.get("id");
    console.log("  id : ", id);
    const mode = searchParams.get("mode");
    console.log("  mode : ", mode);

    return (
        <div>
            <h1>Edit</h1>
            <p>이곳은 일기 수정 페이지 입니다.</p>
            <button onClick={() => setSearchParams({ who: "winterlood" })}>OS 바꾸기</button>
        </div>
    );
};

export default Edit;

 

 

 

 

3.  useNavigate

 

후크 는 useNavigate예를 들어 효과에서 프로그래밍 방식으로 탐색할 수 있는 함수를 반환합니다.

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

function useLogoutTimer() {
  const userIsInactive = useFakeInactiveUser();
  const navigate = useNavigate();

  useEffect(() => {
    if (userIsInactive) {
      fake.logout();
      navigate("/session-timed-out");
    }
  }, [userIsInactive]);
}

 

Edit.js

import * as React from "react";
import { useSearchParams, useNavigate } from 'react-router-dom';


const Edit = () => {
    const navigate = useNavigate();
   

    return (
        <div>
            <h1>Edit</h1>
            <p>이곳은 일기 수정 페이지 입니다.</p>          
            <button onClick={() => navigate("/home")}>
                HOME 으로 가기
            </button>

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

export default Edit;

 

 

 

 

 

 

 

55.프로젝트 기초 공사 1

강의 : https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103543?tab=curriculum

 

 

 

 

1.폰트 세팅

1)Nanum Pen 
2)Yeon Sung

 

@import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&family=Yeon+Sung&display=swap');


.App{
    padding: 20px;
    /* font-family: 'Yeon Sung', 'Nanum Pen Script',  cursive; */
    font-family: 'Nanum Pen Script', cursive;
    font-family: 'Yeon Sung', cursive;
}

 

 

 

2.레이아웃 세팅

 

App.css

@import url('https://fonts.googleapis.com/css2?family=Nanum+Pen+Script&family=Yeon+Sung&display=swap');

body{
    background-color: #f6f6f6;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 100vh;
    margin: 0px;
}

@media (min-width: 650px){
    .App{
        width: 640px;
        background-color: antiquewhite;
    }
}

@media (max-width: 650px){
    .App{
        width: 90vh;
        background-color: cadetblue;
    }
}


#root{
    background-color: white;
    box-shadow: rgba(100,100,111,0.2) 0px 7px 29px 0px;

}

.App{
    padding: 20px;
    /* font-family: 'Yeon Sung', 'Nanum Pen Script',  cursive; */
    font-family: 'Nanum Pen Script', cursive;
    font-family: 'Yeon Sung', cursive;
    min-height: 100vh;
    padding: 0px 20px;
}

 

 

 

 

3.이미지 에셋 세팅

App.js

~

function App() {

  //const env = process.env;
  //env.PUBLIC_URL = env.PUBLIC_URL || "";

  return (
    <BrowserRouter>
      <div className="App">
        <h2>App.js</h2>
        <img src={process.env.PUBLIC_URL + `/assets/images/emotion1.png`} />
        <img src={process.env.PUBLIC_URL + `/assets/images/emotion2.png`} />
        <img src={process.env.PUBLIC_URL + `/assets/images/emotion3.png`} />
        <img src={process.env.PUBLIC_URL + `/assets/images/emotion4.png`} />

~

 

 

 

 

 

4.공콩 컴포넌트 세팅

 

모든 페이지에 공통으로 사용되는 버튼, 헤더 컴포넌트 세팅

 

 

App.js

        <MyButton text={"버튼"} onClick={() => alert("버튼 클릭")} type={"positive"} />
        <MyButton text={"버튼"} onClick={() => alert("버튼 클릭")} type={"negative"} />
        <MyButton text={"버튼"} onClick={() => alert("버튼 클릭")} type={"default"} />

 

src/components/MyButton.js

import React from 'react';

const MyButton = ({ text, type, onClick }) => {

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

    return (
        <button className={['MyButton', `MyButton_${btnType}`].join(" ")} onClick={onClick}>
            {text}
        </button>
    );
};

export default MyButton;

 

App.css

/* MyButton */
.MyButton{
    cursor: pointer;
    border: none;
    border-radius: 5px;

    padding-top: 10px;
    padding-bottom: 10px;    
    padding-left: 20px;
    padding-right: 20px;

    font-size: 18px;

    white-space:nowrap;
    font-family: 'Nanum Pen Script';
}

.MyButton_default{
    background-color: #e2e2e2;
    color: #000;
}

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

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

 

 

 

 

 

 

 

 

 

 

App.js

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

        />

~

 

src/components/MyHeader.js

import React from 'react';

const MyHeader = ({ headText, leftChild, rightChild }) => {
    return (
        <header>
            <div className='head_btn_left'>
                {leftChild}
            </div>
            <div className='head_text'>
                {headText}
            </div>
            <div className='head_btn_right'>
                {rightChild}
            </div>
        </header>
    );
};

export default MyHeader;

 

App.css

/* HEADER */
header{
    padding: 20px 0px;

    display: flex;
    align-items: center;
    border-bottom: 1px solid #e2e2e2;
}

header > div{
    display: flex;
}

header .head_text{
    width: 50%;
    font-size: 25px;
    justify-content: center;
}

header .head_btn_left{
    width: 25%;
    justify-content: start;
}

header .head_btn_right{
    width: 25%;
      justify-content: end;
}

header button{
    font-family: "Nanum Pen Script";
}

 

 

 

 

 

 

 

 

 

56.프로젝트 기초 공사 2

 

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103544?tab=curriculum

 

 

 

 

 

App.js

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

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

//COMPONENTS
import MyButton from './components/MyButton';
import MyHeader from './components/MyHeader';



const reducer = (state, action) => {
  let newState = [];
  switch (action.type) {
    case "INIT":
      return dummyData;

    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(7);

  useEffect(() => {
    dispatch({ type: "INIT" });

  }, []);


  //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: "RMOVE", 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="/" element={<Home />} />
              <Route path="/new" element={<New />} />
              <Route path="/edit" element={<Edit />} />
              <Route path="/diary/:id" element={<Diary />} />
            </Routes>
          </div>
        </BrowserRouter >
      </DiaryDispatchContext.Provider>
    </DiaryStateContext.Provider>
  );
}

export default App;

 

 

 

 

 

 

57.페이지 구현 - 홈 (/)

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103545?tab=curriculum

 

 

 

~

const dummyData = [
  {
    id: 1,
    emotion: 1,
    content: "오늘의일기 1번",
    date: 1668237921756
  },
  {
    id: 2,
    emotion: 2,
    content: "오늘의일기 2번",
    date: 1668237921757
  },
  {
    id: 3,
    emotion: 3,
    content: "오늘의일기 3번",
    date: 1668237921758
  },
  {
    id: 4,
    emotion: 4,
    content: "오늘의일기 4번",
    date: 1668237921759
  },
  {
    id: 5,
    emotion: 5,
    content: "오늘의일기 5번",
    date: 1668237921760
  },
  {
    id: 6,
    emotion: 2,
    content: "오늘의일기 6번",
    date: 176823792180
  },
]

function App() {

~

 

 

src/components/DiaryItem.js

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



const DiaryItem = ({ id, content, emotion, date }) => {
    const navigate = useNavigate();

    const env = process.env;
    env.PUBLIC_URL = env.PUBLIC_URL || "";

    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/images/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>
    );
};

export default DiaryItem;

 

 

 

src/components/DiaryList.js

import React, { useState } from 'react';
import MyButton from './MyButton';
import { useNavigate } from 'react-router-dom';
import DiaryItem from './DiaryItem';

const sortOptionList = [
    { value: "lastest", name: "최신순" },
    { value: "oldest", name: "오래된 순" }
];

const filterOptionList = [
    { value: "all", name: "전부다" },
    { value: "good", name: "좋은 감정만" },
    { value: "bad", name: "안좋은 감정만" },
]

const ControlMenu = ({ value, setOnChange, optionList }) => {
    return <select
        className='ControlMenu'
        value={value} onChange={(e) => setOnChange(e.target.value)} >
        {
            optionList.map((it, idx) =>
                (<option key={idx} value={it.value}>{it.name}</option>))
        }
    </select>
}

const DiaryList = ({ diaryList }) => {
    const navigate = useNavigate();
    const [sortType, setSortType] = useState('lastest');
    const [filter, setFilter] = useState("all");


    const getProcessedDiaryList = () => {

        const filterCallBack = (item) => {
            if (filter === 'good') {
                return parseInt(item.emotion) <= 3;
            } else {
                return parseInt(item.emotion) > 3;
            }
        }

        //오래된순 &&  최신순
        const compare = (a, b) => {
            if (sortType === "lastest") {
                return parseInt(b.date) - parseInt(a.date);
            } else {
                return parseInt(a.date) - parseInt(b.date);
            }
        }
        const copyList = JSON.parse(JSON.stringify(diaryList));

        const filteredList = filter === 'all' ? copyList : copyList.filter((it) => filterCallBack(it));

        const sortList = filteredList.sort(compare);
        return sortList;
    }


    return (
        <div className='DiaryList'>
            <div className='menu_wrapper'>
                <div className="left_col">
                    <ControlMenu value={sortType} setOnChange={setSortType} optionList={sortOptionList} />
                    <ControlMenu value={filter} setOnChange={setFilter} optionList={filterOptionList} />
                </div>
                <div className="right_col">

                    <MyButton type={'positive'} text={"새읽기 쓰기"} onClick={() => navigate("/new")} />
                </div>
            </div>





            {getProcessedDiaryList() && getProcessedDiaryList().map((it) => (
                <DiaryItem key={it.id}  {...it} />
            ))}
        </div>
    );
};

DiaryList.defaultProps = {
    diaryList: [],
}
export default DiaryList;

 

 

 

 

src/components/Home.js

import React, { useContext, useEffect, useState } from 'react';
import MyButton from '../components/MyButton';
import MyHeader from './../components/MyHeader';
import { DiaryStateContext } from './../App';
import DiaryList from './../components/DiaryList';

const Home = () => {

    const diaryList = useContext(DiaryStateContext);


    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();

            // console.log(" firstDay : lastDay ", firstDay, lastDay);

            setData(diaryList.filter((it) => firstDay <= it.date && it.date <= lastDay));

        }
    }, [diaryList, curDate]);


    useEffect(() => {
        //console.log("data : ", data);

    }, [data]);


    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={<div onClick={() => setCurDate(new Date())} style={{ cursor: "pointer" }} >{headText}</div>}
                leftChild={<MyButton text={"<"} onClick={() => { decreaseMonth() }} />}
                rightChild={<MyButton text={">"} onClick={() => { increaseMonth() }} />}
            />

            <DiaryList diaryList={data} />

        </div>
    );
};

export default React.memo(Home);

 

 

 

 

 

 

 

 

 

58.페이지 구현 - 일기 쓰기 (/new)

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103546?tab=curriculum

 

 

 

DiaryEditor.js

import React, { useState, useRef, useContext } from 'react';
import MyHeader from './MyHeader';
import MyButton from './MyButton';
import { useNavigate } from 'react-router-dom';
import EmotionItem from './EmotionItem';
import { DiaryDispatchContext } from '../App';

const env = process.env;
env.PUBLIC_URL = env.PUBLIC_URL || "";


const emotionList = [
    {
        emotion_id: 1,
        emotion_img: process.env.PUBLIC_URL + `/assets/images/emotion1.png`,
        emotion_descript: '완전 좋음'
    },
    {
        emotion_id: 2,
        emotion_img: process.env.PUBLIC_URL + `/assets/images/emotion2.png`,
        emotion_descript: '좋음'
    },
    {
        emotion_id: 3,
        emotion_img: process.env.PUBLIC_URL + `/assets/images/emotion3.png`,
        emotion_descript: '그럭저럭'
    },
    {
        emotion_id: 4,
        emotion_img: process.env.PUBLIC_URL + `/assets/images/emotion4.png`,
        emotion_descript: '나쁨'
    },
    {
        emotion_id: 5,
        emotion_img: process.env.PUBLIC_URL + `/assets/images/emotion5.png`,
        emotion_descript: '끔찍함'
    },
]


export const getStringDate = (date) => {

    let year = date.getFullYear();
    let month = date.getMonth() + 1;
    let day = date.getDate();
    if (month < 10) {
        month = `0${month}`;
    }

    if (day < 10) {
        day = `0${day}`;
    }
    return `${year}-${month}-${day}`;
};

const DiaryEditor = () => {

    const contentRef = useRef();
    const [content, setContent] = useState("");
    const [emotion, setEmotion] = useState(3);
    const [date, setDate] = useState(getStringDate(new Date()));

    const navigate = useNavigate();
    const { onCreate } = useContext(DiaryDispatchContext);

    const handleClickEmote = (emotion) => {
        setEmotion(emotion);
    }

    const hadnleSubmit = () => {
        if (content.length < 1) {
            contentRef.current.focus();
            return;
        }
        console.log("hadnleSubmit  : ", date, content, emotion);
        onCreate(date, content, emotion);
        navigate("/", { replace: true });
    }

    return (
        <div className='DiaryEditor'>
            <MyHeader headText={"새 일기쓰기"} leftChild={<MyButton text={"< 뒤로가기"} onClick={() => navigate(-1)} />} />

            <div>
                <section>
                    <h4>오늘은 언제인가요?</h4>
                    <div className='input_box'>
                        <input
                            className='input_date'
                            value={date}
                            onChange={(e) => setDate(e.target.value)}
                            type="date" />
                    </div>
                </section>

                <section>
                    <h4>오늘의 감정</h4>
                    <div className='input_box emotion_list_wrapper'>
                        {emotionList.map((it) => (
                            <EmotionItem key={it.emotion_id}  {...it}
                                onClick={handleClickEmote}
                                isSelected={it.emotion_id === emotion}
                            />
                        ))}
                    </div>
                </section>

                <section>
                    <h4>오늘의 일기</h4>
                    <div className='input_box text_wrapper'>
                        <textarea
                            placeholder='오늘은 어땠나요'
                            ref={contentRef}
                            value={content}
                            onChange={(e) => setContent(e.target.value)}
                        />
                    </div>
                </section>


                <section>
                    <div className='control_box'>
                        <MyButton text={'취소하기'} onClick={() => navigate(-1)} />
                        <MyButton text={'작성완료'} type={"positive"} onClick={hadnleSubmit} />
                    </div>
                </section>

            </div >



        </div >
    );
};

export default DiaryEditor;

 

 

 

EmotionItem.js

import { useState } from 'react';

const EmotionItem = ({ emotion_id, emotion_img, emotion_descript, onClick, isSelected }) => {



    return <div className={["EmotionItem", isSelected ? `EmotionItem_on_${emotion_id}` : "EmotionItem_off"].join(" ")}
        onClick={() => onClick(emotion_id)}>

        <img src={emotion_img} />
        <span>{emotion_descript}</span>

    </div>
};

export default EmotionItem;

 

 

 

New.js

import React, { useState } from 'react';
import DiaryEditor from '../components/DiaryEditor';


const New = () => {


    return (
        <div>
            <DiaryEditor />

        </div>
    );

};

export default React.memo(New);

 

  App.css

/* DiaryEditor */



.DiaryEditor section{
    margin-bottom: 40px;
}

.DiaryEditor h4{
    font-size: 22px;
    font-weight: bold;
}


.DiaryEditor .input_date{
    border: none;
    border-radius: 5px;
    background-color: #ececec;

    padding:10px 20px;

    cursor: pointer;
    font-family: "Nanum Pen Script";
    cursor: pointer;
    font-size: 20px;
}



.DiaryEditor .emotion_list_wrapper{
    display: grid;
    grid-template-columns: repeat(5, auto);
    gap:2%
}


.DiaryEditor textarea{
    font-family:"Nanum Pen Script";
    font-size: 18px;

    box-sizing: border-box;
    width: 100%;
    min-height: 200px;
    resize: vertical;

    border: none;
    border-radius: 5px;  
    background-color: #ececec;
    padding: 20px;
}

.DiaryEditor .control_box{
    display: flex;
    justify-content: space-between;
    align-items: center;
    
}


/* EmotionItem */
.EmotionItem{
    cursor: pointer;
    border-radius: 5px;
    padding-top: 20px;
    padding-bottom: 20px;
    
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
}

.EmotionItem img{
    width: 50%;
    margin-bottom: 10px;
}
.EmotionItem span{
    font-size: 18px;
}

.EmotionItem_off{
    background-color: #ececec;
}
.EmotionItem_on_1{
    background-color: #64c964;
     color: #fff;
}
.EmotionItem_on_2{
    background-color: #9dd772;
     color: #fff;
}
.EmotionItem_on_3{
    background-color: #fdce17;
     color: #fff;
}
.EmotionItem_on_4{
    background-color: #fd8446;
     color: #fff;
}
.EmotionItem_on_5{
    background-color: #fd565f;
     color: #fff;
}

 

 

 

 

 

 

 

 

 

 

59.페이지 구현 - 일기 수정 (/edit)

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103547?tab=curriculum

 

 

소스 :

https://github.com/braverokmc79/react-emotion-diary/commit/75284bd71287a463fbf4898b17730a6b56d3039a

 

DiaryEditor.js

 

import React, { useState, useRef, useContext, useEffect } from 'react';


~


const DiaryEditor = ({ isEdit, originData }) => {

~

    const { onCreate, onEdit } = useContext(DiaryDispatchContext);

~

    const hadnleSubmit = () => {
        if (content.length < 1) {
            contentRef.current.focus();
            return;
        }
        console.log("hadnleSubmit  : ", date, content, emotion);

        if (window.confirm(isEdit ? "일기를 수정하시겠습니까?" : "새로운 일기를 작성하시겠습니까?")) {

            if (!isEdit) {
                onCreate(date, content, emotion);

            } else {
                onEdit(originData.id, date, content, emotion);
            }
        }
        navigate("/", { replace: true });
    }


    useEffect(() => {
        if (isEdit) {
            setDate(getStringDate(new Date(parseInt(originData.date))));
            setEmotion(originData.emotion);
            setContent(originData.content);
        }
    }, [isEdit, originData])

    return (
        <div className='DiaryEditor'>
            <MyHeader headText={isEdit ? '일기 수정하기' : "새 일기쓰기"} leftChild={<MyButton text={"< 뒤로가기"} onClick={() => navigate(-1)} />} />

         ~

        </div >
    );
};

export default DiaryEditor;

 

 

 

Edit.js

import { useContext, useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { useParams } from 'react-router-dom';
import DiaryEditor from '../components/DiaryEditor';
import { DiaryStateContext } from './../App';



const Edit = () => {
    const [originData, setOriginData] = useState();
    const navigate = useNavigate();
    const { id } = useParams();
    const diaryList = useContext(DiaryStateContext);


    useEffect(() => {
        if (diaryList.length >= 1) {
            const targetDiary = diaryList.find((it) => parseInt(it.id) === parseInt(id))

            console.log(targetDiary);
            if (targetDiary) {
                setOriginData(targetDiary);

            } else {
                navigate('/', { replace: true });
            }
        }

    }, [id, diaryList]);

    return (
        <div>
            {originData && <DiaryEditor isEdit={true} originData={originData} />}
        </div>
    );
};

export default Edit;

 

 

 

 

 

 

 

 

60.페이지 구현 - 일기 상세 (/diary)

강의 :  https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103548?tab=curriculum

 

소스 : 

https://github.com/braverokmc79/react-emotion-diary/commit/c83f4a5edf1b7e354769431452df36056e521c0d

 

 

Diary.js

import React, { useContext, useEffect, useState } from 'react';
import { useNavigate, useParams } from "react-router-dom";
import MyButton from '../components/MyButton';
import MyHeader from '../components/MyHeader';
import { DiaryStateContext } from './../App';
import { getStringDate } from './../utils/date';
import { emotionList } from './../utils/emotion';


const Diary = () => {
    const [data, setData] = useState();
    const diaryList = useContext(DiaryStateContext);
    const navigate = useNavigate();

    const { id } = useParams();
    // console.log("contextcontext ", diaryList);

    useEffect(() => {

        if (diaryList) {
            const targetDiary = diaryList.find((it) => parseInt(it.id) === parseInt(id));
            //console.log(targetDiary);
            if (targetDiary) {
                setData(targetDiary);
            } else {
                alert("없는 일기 입니다.");
                navigate('/', { replace: true });
            }
        }

    }, [id, diaryList]);


    if (!data) {
        return <div className='DiaryPage' >로딩중입니다...</div>
    } else {

        const curEmotionData = emotionList.find((it) => parseInt(it.emotion_id) === parseInt(data.emotion));
        console.log(curEmotionData)

        return (
            <div className='DiaryPage'>
                <MyHeader headText={`${getStringDate(new Date(data.date))} 기록`}
                    leftChild={<MyButton text={("< 뒤로가기")} onClick={() => navigate(-1)} />}
                    rightChild={<MyButton text={("수정하기")} onClick={() => navigate(`/edit/${data.id}`)} />}
                />


                <article>
                    <section>
                        <h4>오늘의 감정</h4>
                        <div className={
                            ["diary_img_wrapper", `diary_img_wrapper_${data.emotion}`].join(" ")
                        }>
                            <img src={curEmotionData.emotion_img} />
                            <div className='emotion_descript'>
                                {curEmotionData.emotion_descript}
                            </div>
                        </div>
                    </section>

                    <section>
                        <h4>오늘의 일기</h4>
                        <div className='diary_content_wrapper'>
                            <p>{data.content}</p>
                        </div>
                    </section>

                </article>

            </div >
        )
    }
};

export default Diary;

 

 

App.css

/* Diary */

.DiaryPage {

}

.DiaryPage section{
    width: 100%;
    margin-bottom: 100px;

    display: flex;
    flex-direction: column;
    align-items: center;
    text-align: center;
}


.DiaryPage h4{
    font-size: 22px;
    font-weight: bold;
}

.DiaryPage .diary_img_wrapper{
    background-color: #ececec;
    width: 250px;
    height: 250px;

    border-radius: 5px;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: space-around;
}


.DiaryPage .emotion_descript{
    font-size: 25px;
}


.DiaryPage .diary_img_wrapper_1{
    background-color: #64c964;
     color: #fff;
}
.DiaryPage .diary_img_wrapper_2{
    background-color: #9dd772;
     color: #fff;
}
.DiaryPage .diary_img_wrapper_3{
    background-color: #fdce17;
     color: #fff;
}
.DiaryPage .diary_img_wrapper_4{
    background-color: #fd8446;
     color: #fff;
}
.DiaryPage .diary_img_wrapper_5{
    background-color: #fd565f;
     color: #fff;
}


.DiaryPage .diary_content_wrapper {
    width: 100%;
    background-color: #ececec;
    border-radius: 5px;
    word-break: keep-all;
    overflow-wrap:break-word ;
}


.DiaryPage .diary_content_wrapper p{
    padding: 20px;
    text-align: left;
    font-size: 20px;
    font-family: 'Yeon Sung';
    font-weight: 400;
    line-height: 2.5;
}

 

 

 

 

 

 

 

 

61.(서브 챕터) 흔히 발생하는 버그 수정 하기

 

 강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103549?tab=curriculum

 

 

 

 

62.LocalStorage를 일기 데이터베이스로 사용하기

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103550?tab=curriculum

 

 

삭제하기

DiaryEditor.js

    const hadnleSubmit = () => {
        if (content.length < 1) {
            contentRef.current.focus();
            return;
        }
        console.log("hadnleSubmit  : ", date, content, emotion);

        if (window.confirm(isEdit ? "일기를 수정하시겠습니까?" : "새로운 일기를 작성하시겠습니까?")) {

            if (!isEdit) {
                onCreate(date, content, emotion);

            } else {
                onEdit(originData.id, date, content, emotion);
            }
        }
        navigate("/", { replace: true });
    }

~


        <div className='DiaryEditor'>
            <MyHeader headText={isEdit ? '일기 수정하기' : "새 일기쓰기"}
                leftChild={<MyButton text={"< 뒤로가기"} onClick={() => navigate(-1)} />}

                rightChild={<MyButton text={"삭제하기"} type={"negative"}
                    onClick={removeHandler} />}
            />


 

 

localStorage 사용법

 

    //localStorage 저장하기
    // localStorage.setItem("item1");
    // localStorage.setItem("item2");
    // localStorage.setItem("item3");

    //localStorage 가져오기
    // const item1 = localStorage.getItem("item1");
    // const item2 = localStorage.getItem("item2");
    // const item3 = JSON.parse(localStorage.getItem("item3"));
    // console.log({ item1, item2, item3 })

 

App.js

  useEffect(() => {


    //localStorage 저장하기
    // localStorage.setItem("item1");
    // localStorage.setItem("item2");
    // localStorage.setItem("item3");

    //localStorage 가져오기
    // const item1 = localStorage.getItem("item1");
    // const item2 = localStorage.getItem("item2");
    // const item3 = JSON.parse(localStorage.getItem("item3"));
    // console.log({ item1, item2, item3 })


    const localData = localStorage.getItem('diary');
    if (localData) {
      const diaryList = JSON.parse(localData).sort((a, b) => parseInt(b.id) - parseInt(a.id));
      dataId.current = parseInt(diaryList[0].id) + 1;
      console.log("  dataId.current : ", dataId.current);
      dispatch({ type: "INIT", data: diaryList });
    }

  }, []);

 

 

 

 

 

 

 

 

 

 

 

 

 

 

63.프로젝트 최적화

 

 

강의 :

https://www.inflearn.com/course/%ED%95%9C%EC%9E%85-%EB%A6%AC%EC%95%A1%ED%8A%B8/unit/103551?tab=curriculum

 

 

소스 :

https://github.com/braverokmc79/react-emotion-diary/commit/edc054f5e0fea9d6abcf080e095a804b0db6268b

 

 

 

 

 

 

 

 

 

 

 

about author

PHRASE

Level 60  라이트

가장 나쁜 사람이 누군가? 그대가 줏대 없이 이리저리 마음이 흔들리도록 꼬드기는 사람이다. -소크라테스

댓글 ( 4)

댓글 남기기

작성