


npm i react-router-domimport { BrowserRouter } from "react-router-dom";
createRoot(document.getElementById("root")).render(
<BrowserRouter>
<App />
</BrowserRouter>
);
BrowserRouter -> Navigation.Provider나 Location.Provider를 모든 React 컴포넌트들에게 사용할 수 있게 함import "./App.css";
import { Routes, Route } from "react-router-dom";
import Home from "./pages/Home";
import Diary from "./pages/Diary";
import New from "./pages/New";
import Notfound from "./pages/Notfound";
// 1. "/" : 모든 일기를 조회하는 Home 페이지
// 2. "/new" : 새로운 일기를 작성하는 New 페이지
// 3. "/diary" : 일기를 상세히 조회하는 Diary 페이지
function App() {
return (
<>
<div>Hello</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary" element={<Diary />} />
<Route path="*" element={<Notfound />} />
</Routes>
</>
);
}
export default App;
Routes 컴포넌트 안에는 Route 컴포넌트만 쓸 수 있음Routes 컴포넌트 바깥에는 모든 페이지에 동일하게 렌더링이 되는 페이지를 작성할 수 있다.<a/>태그를 대체<a/>태그는 서버사이드렌더링navigate함수import "./App.css";
import { Routes, Route, Link, useNavigate } from "react-router-dom";
import Home from "./pages/Home";
import Diary from "./pages/Diary";
import New from "./pages/New";
import Notfound from "./pages/Notfound";
// 1. "/" : 모든 일기를 조회하는 Home 페이지
// 2. "/new" : 새로운 일기를 작성하는 New 페이지
// 3. "/diary" : 일기를 상세히 조회하는 Diary 페이지
function App() {
const nav = useNavigate();
const onClickButton = () => {
nav("/new");
};
return (
<>
<div>
<Link to={"/"}>HOME</Link>
<Link to={"/new"}>New</Link>
<Link to={"/diary"}>Diary</Link>
<button onClick={onClickButton}>New 페이지로 이동</button>
</div>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary" element={<Diary />} />
<Route path="*" element={<Notfound />} />
</Routes>
</>
);
}
export default App;

~/product/1/ 뒤에 아이템 id와 같이 변경되지 않는 값을 주소로 명시하기 위해 사용됨~/search?q=검색어? 뒤에 변수명과 값 표시useParams...
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary/:id" element={<Diary />} />
<Route path="*" element={<Notfound />} />
</Routes>
...
import { useParams } from "react-router-dom";
const Diary = () => {
const params = useParams();
console.log(params);
return <div>{params.id}번 일기입니다 ~~</div>;
};
export default Diary;
useSearchParamsimport { useSearchParams } from "react-router-dom";
const Home = () => {
const [params, setParams] = useSearchParams();
console.log(params.get("value"));
return <div>Home</div>;
};
export default Home;
publicsrc/assetspublic 폴더는 import를 통해 불러올 수 없지만 URL을 통해 불러올 수 있다.npm run build -> 파일 탐색기에 dist 폴더가 생김npm run preview -> 빌드된 결과물 보기public폴더에 있던 이미지 파일은 /emotion1.png와 같은 일반적인 주소인 반면, src/assets에 있던 파일은 data:image/png:base64 ...와 같은 암호문 같은 포맷(Data URI)으로 설정됨import emotion1 from "./../assets/emotion1.png";
import emotion2 from "./../assets/emotion2.png";
import emotion3 from "./../assets/emotion3.png";
import emotion4 from "./../assets/emotion4.png";
import emotion5 from "./../assets/emotion5.png";
export function getEmotionImage(emotionId) {
switch (emotionId) {
case 1:
return emotion1;
case 2:
return emotion2;
case 3:
return emotion3;
case 4:
return emotion4;
case 5:
return emotion5;
default:
null;
}
}
import { getEmotionImage } from "./util/get-emotion-images";
function App() {
return (
<>
<div>
<img src={getEmotionImage(1)} />
<img src={getEmotionImage(2)} />
<img src={getEmotionImage(3)} />
<img src={getEmotionImage(4)} />
<img src={getEmotionImage(5)} />
</div>
</>
}
const mockData = [
{
id: 1,
createdDate: new Date().getTime(),
emotionId: 1,
content: "1번 일기 내용",
},
{
id: 2,
createdDate: new Date().getTime(),
emotionId: 2,
content: "2번 일기 내용",
},
];
function reducer(state, action) {
return state;
}
function App() {
const [data, dispatch] = useReducer(reducer, mockData);
}
function reducer(state, action) {
switch (action.type) {
case "CREATE":
return [action.data, ...state];
case "UPDATE":
return state.map((item) =>
String(item.id) === String(action.data.id) ? action.data : item
);
case "DELETE":
return state.filter((item) => String(item.id) !== String(action.id));
default:
state;
}
}
...
// 새로운 일기 추가
const onCreate = (createdDate, emotionId, content) => {
dispatch({
type: "CREATE",
data: {
id: idRef.current++,
createdDate,
emotionId,
content,
},
});
};
// 기존 일기 수정
const onUpdate = (id, createdDate, emotionId, content) => {
dispatch({
type: "UPDATE",
data: {
id,
createdDate,
emotionId,
content,
},
});
};
// 기존 일기 삭제
const onDelete = (id) => {
dispatch({
type: "DELETE",
id,
});
};
const DiaryStateContext = createContext();
const DiaryDispatchContext = createContext();
<DiaryStateContext.Provider value={data}>
<DiaryDispatchContext.Provider value={{ onCreate, onUpdate, onDelete }}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/new" element={<New />} />
<Route path="/diary/:id" element={<Diary />} />
<Route path="/edit/:id" element={<Edit />} />
<Route path="*" element={<Notfound />} />
</Routes>
</DiaryDispatchContext.Provider>
</DiaryStateContext.Provider>
const beginTime = new Date(
pivotDate.getFullYear(), // 연
pivotDate.getMonth(), // 월
1, // 일
0, // 시
0, // 분
0 // 초
).getTime();
const endTime = new Date(
pivotDate.getFullYear(),
pivotDate.getMonth() + 1,
0, // 0일 = 이전달의 마지막 일
23,
59,
59
).getTime();
Array.prototype.sort() : 원본 배열을 정렬toSorted() : 정렬된 새로운 배열을 반환const getSortedDate = () => {
return data.toSorted()
}
<section/> : div 태그와 기능이 같음.Editor > section {
margin-bottom: 40px;
}
body { display : flex; } 로 해결nav(-1)import Header from "../components/Header";
import Button from "../components/Button";
import Editor from "../components/Editor";
import { useNavigate } from "react-router-dom";
const New = () => {
const nav = useNavigate();
return (
<div>
<Header
title={"새 일기쓰기"}
leftChild={<Button onClick={() => nav(-1)} text={"< 뒤로 가기"} />}
/>
<Editor />
</div>
);
};
export default New;
<input value={new Date()}/> 태그는 날짜를 인식할 수 없어서 문자열로 변환하여 value속성에 넣어주어야한다.nav("/", { replace: true });const Edit = () => {
...
const getCurrentDiaryItem = () => {
const currentDiaryItem = data.find(
(item) => String(item.id) === String(params.id)
);
if (!currentDiaryItem) {
window.alert("존재하지 않는 일기입니다.");
nav("/", { replace: true });
}
return currentDiaryItem;
};
...
return <> ... </>;
}

nav("/", { replace : true })는 Edit 컴포넌트가 마운트되기전에 실행되어 위와 같은 에러를 뱉는다.
const Edit = () => {
return (
<div>
<Editor initData={curDiaryItem} />
</div>
);
};
export default Edit;
createdDate : 타임스탬프 값인 숫자 값이 들어가 버리게 되면서 오류가 발생할 수 있음. getStringedData()같은 함수에서는 createdDate값을 데이트 객체라고 상정하기 때문.const Editor = ({ initData, onSubmit }) => {
const [input, setInput] = useState({ ... });
useEffect(() => {
if (initData) {
setInput({
...initData,
createdDate: new Date(Number(initData.createdDate)),
});
}
}, [initData]);
...
}
import { useContext, useState, useEffect } from "react";
import { DiaryStateContext } from "../App";
import { useNavigate } from "react-router-dom";
const useDiary = (id) => {
const data = useContext(DiaryStateContext);
const [curDiaryItem, setCurDiaryItem] = useState();
const nav = useNavigate();
useEffect(() => {
const currentDiaryItem = data.find(
(item) => String(item.id) === String(id)
);
if (!currentDiaryItem) {
window.alert("존재하지 않는 일기입니다.");
nav("/", { replace: true });
}
setCurDiaryItem(currentDiaryItem);
}, [id, data]);
return curDiaryItem;
};
export default useDiary;

useMemo, useCallback, React.memo 기능들은 과하게 사용하게 되면 컴포넌트의 함수의 확장성이 줄어들어 버린다거나 프로젝트의 유지보수를 어렵게 많드는 등 독이 될 수 있음.SessionStorage, LocalStorage : 데이터를 어디 보관하는지, 언제 초기화되는지에 차이가 있음function reducer(state, action) {
let nextState;
switch (action.type) {
case "INIT":
return action.data;
case "CREATE":
{
nextState = [action.data, ...state];
}
break;
case "UPDATE":
{
nextState = state.map((item) =>
String(item.id) === String(action.data.id) ? action.data : item
);
}
break;
case "DELETE":
{
nextState = state.filter(
(item) => String(item.id) !== String(action.id)
);
}
break;
default:
return state;
}
localStorage.setItem("diary", JSON.stringify(nextState));
return nextState;
}
...
function App() {
const [data, dispatch] = useReducer(reducer, []);
const idRef = useRef(0);
useEffect(() => {
const storedData = localStorage.getItem("diary");
if (!storedData) {
return;
}
const parsedData = JSON.parse(storedData);
if (!Array.isArray(parsedData)) {
return;
}
let maxId = 0;
parsedData.forEach((item) => {
if (Number(item.id) > maxId) {
maxId = Number(item.id);
}
});
idRef.current = maxId + 1;
dispatch({
type: "INIT",
data: parsedData,
});
}, []);
...
}
const [data, dispatch = useReducer(reducer, []); 의 값을 참조함.
function App() {
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
... // data init
setIsLoading(false);
}, []);
<DiaryStateContext.Provider value={data}>
... // Contexts, Routes
</DiaryStateContext.Provider>
}

import { useEffect } from "react";
const usePageTitle = (title) => {
useEffect(() => {
const $title = document.getElementsByTagName("title")[0];
$title.innerText = title;
}, [title]);
};
export default usePageTitle;
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
...
</head>
</html>
favicon 적용이 안될때
- Element 속성에서 index.html 파일이 바뀐걸 확인했지만 network 탭에서 favicon.ico 파일을 불러오지 않았다. 크롬 업데이트를 하니까 됐다.
<meta property="og:title" content="감정 일기장" />
<meta property="og:description" content="나만의 작은 감정 일기장" />
<meta property="og:image" content="/thumbnail.png" />
npm run build


npm install -g vercelvercel loginvercel
