import DayList from "./component/DayList";
import Header from "./component/Header";
import Day from "./component/Day";
import { BrowserRouter, Route, Routes } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<div className="App">
<Header />
<Routes>
<Route exact path="/" element={<DayList />} />
<Route path="/day" element={<Day />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;→ 강의에서는 switch 사용 but 오류 발생
→ Routes로 변경 후 element 사용 → 해결
→ 버전 차이?
import { Link } from "react-router-dom";
export default function Header() {
return (
<div className="header">
<h1>
<Link to="/">토익 영단어(고급)</Link>
</h1>
<div className="menu">
<a href="#x" className="link">
단어 추가
</a>
<a href="#x" className="link">
Day 추가
</a>
</div>
</div>
);
}→ import { Link } from "react-router-dom"; : 라우터의 Link 기능 임포트
→ <Link to="/"> : 처음의 화면
import { Link } from "react-router-dom";
import dummy from "../db/data.json";
export default function DayList() {
console.log(dummy);
return (
<ul className="list_day">
{dummy.days.map((day) => (
<li key={day.id}>
<Link to={`/day/${day.day}`}>Day {day.day}</Link>
</li>
))}
</ul>
);
}→ {/day/${day.day}} : 각 day 별 페이지로 연결
import dummy from "../db/data.json";
import { useParams } from "react-router-dom";
export default function Day() {
// dummy.words;
const day = useParams().day;
const wordList = dummy.words.filter((word) => word.day === Number(day));
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{wordList.map((word) => (
<tr key={word.id}>
<td>{word.eng}</td>
<td>{word.kor}</td>
</tr>
))}
</tbody>
</table>
</>
);
}→ useParams : 각 day별 단어를 가져올 수 있게 하는 기능
→ Number(day) : json에 day를 숫자값으로 저장해줬기 때문에 숫자로 입력해야 데이터 사용 가능
import { Link } from "react-router-dom";
export default function EmptyPage() {
return (
<>
<h2>잘못된 접근입니다.</h2>
<Link to="/">돌아가기</Link>
</>
);
}→ 없는 주소에 접속했을 때 나오는 페이지
import DayList from "./component/DayList";
import Header from "./component/Header";
import Day from "./component/Day";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import EmptyPage from "./component/EmptyPage";
function App() {
return (
<BrowserRouter>
<div className="App">
<Header />
<Routes>
<Route exact path="/" element={<DayList />} />
<Route path="/day/:day" element={<Day />} />
<Route path="*" element={<EmptyPage />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;→ <Route path="*" element={<EmptyPage />} /> : 맨 아래에 없는 주소의 페이지에 대한 정의
import { useState } from "react";
export default function Word({ word }) {
const [isShow, setIsShow] = useState(false);
const [isDone, setIsDone] = useState(word.isDone);
function toggleShow() {
setIsShow(!isShow);
}
function toggleDone() {
setIsDone(!isDone);
}
return (
<tr className={isDone ? "off" : ""}>
<td>
<input type="checkbox" checked={isDone} onChange={toggleDone} />
</td>
<td>{word.eng}</td>
<td>{isShow && word.kor}</td>
<td>
<button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
<button className="btn_del">삭제</button>
</td>
</tr>
);
}→ isShow, isDone → useState로 값 설정
→ toggleShow, toggleDone → 함수로 체크 박스 설정
import dummy from "../db/data.json";
import { useParams } from "react-router-dom";
import Word from "./Word";
export default function Day() {
// dummy.words;
const day = useParams().day;
const wordList = dummy.words.filter((word) => word.day === Number(day));
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{wordList.map((word) => (
<Word word={word} key={word.id} />
))}
</tbody>
</table>
</>
);
}→ import Word from "./Word"; : Word 컴포넌트 임포트
→ <Word word={word} key={word.id} /> : Word 컴포넌트를 key 값을 설정해주며 가져오기
: 명령어 사용 후 port를 3001로 수정하면 json 값이 넘어온 것 확인 가능 (명령어로 port 3001로 설정해줌)
→ method로 CRUD 기능을 하는 것
→ dummy 값을 지우고 json 사용
: Hook → useState와 마찬가지로 리액트에서 임포트
: 어떤 상태값이 바뀌었을 때 동작하는 함수 작성 가능
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
export default function DayList() {
const [days, setDays] = useState([]);
const [count, setCount] = useState(0);
function onClick() {
setCount(count + 1);
}
function onClick2() {
setDays([
...days,
{
id: Math.random(),
day: 1,
},
]);
}
useEffect(() => {
console.log("Count change");
}, []);
return (
<>
<ul className="list_day">
{days.map((day) => (
<li key={day.id}>
<Link to={`/day/${day.day}`}>Day {day.day}</Link>
</li>
))}
</ul>
<button onClick={onClick}>{count}</button>
<button onClick={onClick2}>Day change</button>
</>
);
}useEffect(() => {
console.log("Count change");
}, [count]);
→ count(의존성 배열의 값)이 변경되었을 때만 함수(useEffect)가 실행
→ 의존성 배열
useEffect(() => {
console.log("Count change");
}, []);
→ 현재는 API 호출을 위해 사용하기 때문에 한번만 호출
→ [count] 부분(의존성 배열 부분)을 빈 배열을 입력
import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
export default function DayList() {
const [days, setDays] = useState([]);
useEffect(() => {
fetch("http://localhost:3001/days")
.then((res) => {
return res.json();
})
.then((data) => {
setDays(data);
});
}, []);
return (
<ul className="list_day">
{days.map((day) => (
<li key={day.id}>
<Link to={`/day/${day.day}`}>Day {day.day}</Link>
</li>
))}
</ul>
);
}import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Word from "./Word";
export default function Day() {
const day = useParams().day;
const [words, setWords] = useState([]);
useEffect(() => {
fetch(`http://localhost:3001/words?day=${day}`)
.then((res) => {
return res.json();
})
.then((data) => {
setWords(data);
});
}, [day]);
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{words.map((word) => (
<Word word={word} key={word.id} />
))}
</tbody>
</table>
</>
);
}→ 두 코드의 api를 가져와서 사용하는 useEffect 코드가 비슷
→ Custom Hook 제작 가능
→ src > hooks 폴더 만들기
→ useFetch.js 파일 생성
import { useEffect, useState } from "react";
export default function useFetch(url) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(url)
.then((res) => {
return res.json();
})
.then((data) => {
setData(data);
});
}, [url]);
return data;
}→ 변하는 부분이 url이기 때문에 입력받는 값으로
→ 공통되는 함수이므로 data, setData 로 변경
→ data 값 return
useFetch를 사용하여 수정한 파일
import { Link } from "react-router-dom";
import useFetch from "../hooks/useFetch";
export default function DayList() {
const days = useFetch("http://localhost:3001/days");
return (
<ul className="list_day">
{days.map((day) => (
<li key={day.id}>
<Link to={`/day/${day.day}`}>Day {day.day}</Link>
</li>
))}
</ul>
);
}import { useParams } from "react-router-dom";
import useFetch from "../hooks/useFetch";
import Word from "./Word";
export default function Day() {
const day = useParams().day;
const words = useFetch(`http://localhost:3001/words?day=${day}`);
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{words.map((word) => (
<Word word={word} key={word.id} />
))}
</tbody>
</table>
</>
);
}import { useState } from "react";
export default function Word({ word }) {
const [isShow, setIsShow] = useState(false);
const [isDone, setIsDone] = useState(word.isDone);
function toggleShow() {
setIsShow(!isShow);
}
function toggleDone() {
// setIsDone(!isDone);
fetch(`http://localhost:3001/words/${word.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...word,
isDone: !isDone,
}),
}).then((res) => {
if (res.ok) {
setIsDone(!isDone);
}
});
}
return (
<tr className={isDone ? "off" : ""}>
<td>
<input type="checkbox" checked={isDone} onChange={toggleDone} />
</td>
<td>{word.eng}</td>
<td>{isShow && word.kor}</td>
<td>
<button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
<button className="btn_del">삭제</button>
</td>
</tr>
);
}→ function toggleDone() : useFetch 사용 및 수정 가능하게 변경
→ method: "PUT" : 게시글 수정
→ "Content-Type": "application/json" : 보내는 리소스의 타입이 json
import { useState } from "react";
export default function Word({ word: w }) {
const [word, setWord] = useState(w);
const [isShow, setIsShow] = useState(false);
const [isDone, setIsDone] = useState(word.isDone);
function toggleShow() {
setIsShow(!isShow);
}
function toggleDone() {
// setIsDone(!isDone);
fetch(`http://localhost:3001/words/${word.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...word,
isDone: !isDone,
}),
}).then((res) => {
if (res.ok) {
setIsDone(!isDone);
}
});
}
function del() {
if (window.confirm("삭제 하시겠습니까?")) {
fetch(`http://localhost:3001/words/${word.id}`, {
method: "DELETE",
}).then((res) => {
if (res.ok) {
setWord({ id: 0 });
}
});
}
}
if (word.id === 0) {
return null;
}
return (
<tr className={isDone ? "off" : ""}>
<td>
<input type="checkbox" checked={isDone} onChange={toggleDone} />
</td>
<td>{word.eng}</td>
<td>{isShow && word.kor}</td>
<td>
<button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
<button onClick={del} className="btn_del">
삭제
</button>
</td>
</tr>
);
}→ method: "DELETE" : 게시글 삭제
→ 삭제하는 값은 return 할 값이 없음
→ { word: w } : 새로운 변수명 가능
→ return null; : null 값을 줌으로써 공간을 비우고 새로고침하지 않고도 페이지에 바로 반영
import { Link } from "react-router-dom";
export default function Header() {
return (
<div className="header">
<h1>
<Link to="/">토익 영단어(고급)</Link>
</h1>
<div className="menu">
<Link to="/create_word" className="link">
단어 추가
</Link>
<Link to="/create_day" className="link">
Day 추가
</Link>
</div>
</div>
);
}import DayList from "./component/DayList";
import Header from "./component/Header";
import Day from "./component/Day";
import { BrowserRouter, Route, Routes } from "react-router-dom";
import EmptyPage from "./component/EmptyPage";
import CreateWord from "./component/CreateWord";
import CreateDay from "./component/CreateDay";
function App() {
return (
<BrowserRouter>
<div className="App">
<Header />
<Routes>
<Route exact path="/" element={<DayList />} />
<Route path="/day/:day" element={<Day />} />
<Route path="/create_word" element={<CreateWord />} />
<Route path="/create_day" element={<CreateDay />} />
<Route path="*" element={<EmptyPage />} />
</Routes>
</div>
</BrowserRouter>
);
}
export default App;import { useRef } from "react";
import { useNavigate } from "react-router-dom";
import useFetch from "../hooks/useFetch";
export default function CreateWord() {
const days = useFetch("http://localhost:3001/days");
const navigate = useNavigate();
function onSubmit(e) {
e.preventDefault();
fetch(`http://localhost:3001/words/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
day: dayRef.current.value,
eng: engRef.current.value,
kor: korRef.current.value,
isDone: false,
}),
}).then((res) => {
if (res.ok) {
alert("생성이 완료 되었습니다");
navigate(`/day/${dayRef.current.value}`);
}
});
}
const engRef = useRef(null);
const korRef = useRef(null);
const dayRef = useRef(null);
return (
<form onSubmit={onSubmit}>
<div className="input_area">
<label>Eng</label>
<input type="text" placeholder="computer" ref={engRef} />
</div>
<div className="input_area">
<label>Kor</label>
<input type="text" placeholder="컴퓨터" ref={korRef} />
</div>
<div className="input_area">
<label>Day</label>
<select ref={dayRef}>
{days.map((day) => (
<option key={day.id} value={day.day}>
{day.day}
</option>
))}
</select>
</div>
<button>저장</button>
</form>
);
}→ useRef() : dom에 접근할 수 있게 해줌 (ex. 스크롤 위치 확인, 포커스를 줄 때)
→ current : 해당 요소에 접근 가능
→ vaule : input에 입력된 값을 얻을 수 있음
→ 현재 버전에서 history 지원 X navigate 사용 : 해당 페이지로 이동 기능
import useFetch from "../hooks/useFetch";
import { useNavigate } from "react-router-dom";
export default function CreateWord() {
const days = useFetch("http://localhost:3001/days");
const navigate = useNavigate();
function addDay() {
fetch(`http://localhost:3001/days/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
day: days.length + 1,
}),
}).then((res) => {
if (res.ok) {
alert("생성이 완료 되었습니다");
navigate(`/`);
}
});
}
return (
<div>
<h3>현재 일수 : {days.length}일</h3>
<button onClick={addDay}>Day 추가</button>
</div>
);
}→ 단어 추가와 비슷하지만 day만 추가해주는 기능
import { Link } from "react-router-dom";
import useFetch from "../hooks/useFetch";
export default function DayList() {
const days = useFetch("http://localhost:3001/days");
if (days.length === 0) {
return <span>Loading...</span>;
}
return (
<ul className="list_day">
{days.map((day) => (
<li key={day.id}>
<Link to={`/day/${day.day}`}>Day {day.day}</Link>
</li>
))}
</ul>
);
}import { useParams } from "react-router-dom";
import useFetch from "../hooks/useFetch";
import Word from "./Word";
export default function Day() {
const day = useParams().day;
const words = useFetch(`http://localhost:3001/words?day=${day}`);
return (
<>
<h2>Day {day}</h2>
{words.length === 0 && <span>Loading...</span>}
<table>
<tbody>
{words.map((word) => (
<Word word={word} key={word.id} />
))}
</tbody>
</table>
</>
);
}import { useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import useFetch from "../hooks/useFetch";
export default function CreateWord() {
const days = useFetch("http://localhost:3001/days");
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
function onSubmit(e) {
e.preventDefault();
if (!isLoading) {
setIsLoading(true);
fetch(`http://localhost:3001/words/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
day: dayRef.current.value,
eng: engRef.current.value,
kor: korRef.current.value,
isDone: false,
}),
}).then((res) => {
if (res.ok) {
alert("생성이 완료 되었습니다");
navigate(`/day/${dayRef.current.value}`);
setIsLoading(false);
}
});
}
}
const engRef = useRef(null);
const korRef = useRef(null);
const dayRef = useRef(null);
return (
<form onSubmit={onSubmit}>
<div className="input_area">
<label>Eng</label>
<input type="text" placeholder="computer" ref={engRef} />
</div>
<div className="input_area">
<label>Kor</label>
<input type="text" placeholder="컴퓨터" ref={korRef} />
</div>
<div className="input_area">
<label>Day</label>
<select ref={dayRef}>
{days.map((day) => (
<option key={day.id} value={day.day}>
{day.day}
</option>
))}
</select>
</div>
<button
style={{
opacity: isLoading ? 0.3 : 1,
}}
>
{isLoading ? "Saving..." : "저장"}
</button>
</form>
);
}→ 여러번 클릭하여 빈 값이 추가되는 것을 막는 역할 (Loading 관련 부분)
→ 프로젝트 초기부터 사용하는 것이 좋음
→ but 이후에도 적용 가능하니 늦었다고 생각 X
js 파일 → .ts
jsx 파일 (html 사용 파일) → .tsx로 변경
import { useState } from "react";
interface IProps {
word: IWord;
}
export interface IWord {
day: string;
eng: string;
kor: string;
isDone: boolean;
id: number;
}
export default function Word({ word: w }: IProps) {
const [word, setWord] = useState(w);
const [isShow, setIsShow] = useState(false);
const [isDone, setIsDone] = useState(word.isDone);
function toggleShow() {
setIsShow(!isShow);
}
function toggleDone() {
// setIsDone(!isDone);
fetch(`http://localhost:3001/words/${word.id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...word,
isDone: !isDone,
}),
}).then((res) => {
if (res.ok) {
setIsDone(!isDone);
}
});
}
function del() {
if (window.confirm("삭제 하시겠습니까?")) {
fetch(`http://localhost:3001/words/${word.id}`, {
method: "DELETE",
}).then((res) => {
if (res.ok) {
setWord({
...word,
id: 0,
});
}
});
}
}
if (word.id === 0) {
return null;
}
return (
<tr className={isDone ? "off" : ""}>
<td>
<input type="checkbox" checked={isDone} onChange={toggleDone} />
</td>
<td>{word.eng}</td>
<td>{isShow && word.kor}</td>
<td>
<button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
<button onClick={del} className="btn_del">
삭제
</button>
</td>
</tr>
);
}type → any : 남발 X
interface → 여러개의 property에 각각 다른 타입 입력 가능
typescript → property 사용의 편리성 향상
setWord({ id: 0 }) → id 외의 값들을 옵션(ex. day?: string;)으로 하여 오류 해결
but 삭제 외에는 모든 값들이 필요해서 좋은 방법 X → ...word, 로 다른 값들을 넣어주는 방법으로 해결
export interface → 다른 파일에서도 인터페이스 사용 가능
import { useParams } from "react-router-dom";
import useFetch from "../hooks/useFetch";
import Word, { IWord } from "./Word";
export default function Day() {
// const day = useParams().day;
const { day } = useParams<{ day: string }>();
const words: IWord[] = useFetch(`http://localhost:3001/words?day=${day}`);
return (
<>
<h2>Day {day}</h2>
{words.length === 0 && <span>Loading...</span>}
<table>
<tbody>
{words.map((word) => (
<Word word={word} key={word.id} />
))}
</tbody>
</table>
</>
);
}key={word.id} → word가 IWord라는 것을 못 찾기 때문에 오류 발생
→ Word에서 export로 해주고 임포트하여 설정 후 해결 가능
{ day } → useParams에서 가져오는 객체 내부에 day가 있는지 확신 X로 인한 오류 발생
→ <> generic 속에 타입을 설정하여 오류 해결
import { Link } from "react-router-dom";
import useFetch from "../hooks/useFetch";
export interface IDay {
id: number;
day: number;
}
export default function DayList() {
const days: IDay[] = useFetch("http://localhost:3001/days");
if (days.length === 0) {
return <span>Loading...</span>;
}
return (
<ul className="list_day">
{days.map((day) => (
<li key={day.id}>
<Link to={`/day/${day.day}`}>Day {day.day}</Link>
</li>
))}
</ul>
);
}IDay 라는 인터페이스를 만들어서 사용
import React, { useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import useFetch from "../hooks/useFetch";
import { IDay } from "./DayList";
export default function CreateWord() {
const days: IDay[] = useFetch("http://localhost:3001/days");
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
function onSubmit(e: React.FormEvent) {
e.preventDefault();
if (!isLoading && dayRef.current && engRef.current && korRef.current) {
setIsLoading(true);
const day: dayRef.current.value;
const eng: engRef.current.value;
const kor: korRef.current.value;
fetch(`http://localhost:3001/words/`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
day,
eng,
kor,
isDone: false,
}),
}).then((res) => {
if (res.ok) {
alert("생성이 완료 되었습니다");
navigate(`/day/${day}`);
setIsLoading(false);
}
});
}
}
const engRef = useRef<HTMLInputElement>(null);
const korRef = useRef<HTMLInputElement>(null);
const dayRef = useRef<HTMLSelectElement>(null);
return (
<form onSubmit={onSubmit}>
<div className="input_area">
<label>Eng</label>
<input type="text" placeholder="computer" ref={engRef} />
</div>
<div className="input_area">
<label>Kor</label>
<input type="text" placeholder="컴퓨터" ref={korRef} />
</div>
<div className="input_area">
<label>Day</label>
<select ref={dayRef}>
{days.map((day) => (
<option key={day.id} value={day.day}>
{day.day}
</option>
))}
</select>
</div>
<button
style={{
opacity: isLoading ? 0.3 : 1,
}}
>
{isLoading ? "Saving..." : "저장"}
</button>
</form>
);
}submit하는 이벤트 → React.FormEvent
Ref의 current는 렌더링이 완료된 이후에도 없을 수 있음 → null인지 아닌지 체크해주는 것이 필요
dayRef.current && engRef.current && korRef.current → 값이 없으면 실행하지 X
import { useEffect, useState } from "react";
export default function useFetch(url: string) {
const [data, setData] = useState([]);
useEffect(() => {
fetch(url)
.then((res) => {
return res.json();
})
.then((data) => {
setData(data);
});
}, [url]);
return data;
}💻 라우터 설치 명령어
npm install react-router-dom
💻 json 서버 설치 명령어
npm install -g json-server
💻 서버 위치, port 설정 명령어
json-server --watch ./src/db/data.json --port 3001
→ 두 명령어 동시 사용
💻 typescript 설치 명령어
npm install typescript @types/node @types/react/ @types/react-dom @types/jest
→create-react-app으로 만들어진 프로젝트에 적용하려면 뒤에까지 꼭 입력npm install typescript @types/node @types/react @types/react-dom @types/jest @types/react-router-dom
→ 추가 설치한 라우터 돔까지