코딩앙마
강의 목록 : https://www.youtube.com/playlist?list=PLZKTXPmaJk8J_fHAzPLH8CJ_HO_M33e7-
vscode 확장 패키지 추가
1. - Auto Import - ES6, TS, JSX, TSX
2. - Reactjs code snippets
3. - ESLint
4. - Prettier - Code formatter
Visual Studio Code 폴더/파일 아이콘 변경하기
리액트 프로젝트 생성
npx create-react-app 경로
예) npx create-react-app E:\react-app2
11. json-server, REST API
json-server 설치
$ npm i -g json-server
$ yarn global add json-server
실행
json-server --watch ./src/db/data.json --port 3001
Resources
http://localhost:3001/days
http://localhost:3001/words
REST API
Create : POST
Read : GET
Update : PUT
Delete : DELETE
Day.js
import dummy from '../db/data.json'; import { useLocation, useParams } from 'react-router-dom'; import Word from './Word'; const Day = () => { // const location = useLocation(); // const params = useParams(); // console.log("params ", params, "location ", location); const { day } = useParams(); const wordList = dummy.words.filter(word => { return Number(word.day) === Number(day) }); return ( <> <h2>Day {day}</h2> <table> <tbody> {wordList.map(word => ( <Word key={word.id} word={word} /> ))} </tbody> </table> </> ); }; export default Day;
Word.js
import React from 'react'; import { useState } from 'react'; const Word = ({ word }) => { const [isShow, setIsShow] = useState(false); const [isDone, setIsDone] = useState(word.isDone); function toggleShow() { setIsShow(!isShow); } function toggleDone() { setIsDone(!isDone); } return ( <tr key={word.id} 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> ); }; export default Word;
12. useEffect, fetch()로 API 호출
DayList.js
import { Link } from 'react-router-dom'; import { useEffect, useState } from 'react'; const DayList = () => { const [days, setDays] = useState([]); /* const [count, setCount] = useState(0); function onClick() { setCount(count + 1); } function onClick2() { setDays([...days, { id: new Date(), day: (Math.floor(Math.random() * 31)) + 1, }]); } useEffect(() => { console.log("Counter Chnage"); }, [count]) */ useEffect(() => { fetch("http://localhost:3001/days") .then((res) => res.json()) .then(data => { // console.log("data :", data); setDays(data); }).catch(err => { console.error("에러 :", err); }); }, []); return ( <> <ul className="list_day"> {days.map(day => ( <li key={day.id} id={day.id}> <Link to={`/day/${day.day}`}>Day {day.day}</Link> </li> )) } </ul > {/* <button onClick={onClick}>{count}</button> <button onClick={onClick2}>Day Change</button> */} </> ); }; export default DayList;
Day.js
import { useParams } from 'react-router-dom'; import Word from './Word'; import { useState, useEffect } from 'react'; const Day = () => { const { day } = useParams(); // const wordList = dummy.words.filter(word => { // return Number(word.day) === Number(day) // }); const [words, setWords] = useState([]); useEffect(() => { fetch(`http://localhost:3001/words?day=${day}`) .then((res) => res.json()) .then(data => { //console.log(data); setWords(data); }).catch(err => { console.error("에러:", err); }); }, [day]); return ( <> <h2>Day {day}</h2> <table> <tbody> {words.map(word => ( <Word key={word.id} word={word} /> ))} </tbody> </table> </> ); }; export default Day;
13. Custom Hooks
훅 생성
useFetch.js
import { Link } from 'react-router-dom'; import useFetch from './../hooks/useFetch'; const DayList = () => { const days = useFetch("http://localhost:3001/days"); return ( <> <ul className="list_day"> {days.map(day => ( <li key={day.id} id={day.id}> <Link to={`/day/${day.day}`}>Day {day.day}</Link> </li> )) } </ul > </> ); }; export default DayList;
DayList.js
import { Link } from 'react-router-dom'; import useFetch from './../hooks/useFetch'; const DayList = () => { const days = useFetch("http://localhost:3001/days"); return ( <> <ul className="list_day"> {days.map(day => ( <li key={day.id} id={day.id}> <Link to={`/day/${day.day}`}>Day {day.day}</Link> </li> )) } </ul > </> ); }; export default DayList;
Day.js
import { useParams } from 'react-router-dom'; import Word from './Word'; import useFetch from './../hooks/useFetch'; const Day = () => { const { day } = useParams(); const words = useFetch(`http://localhost:3001/words?day=${day}`); return ( <> <h2>Day {day}</h2> <table> <tbody> {words.map(word => ( <Word key={word.id} word={word} /> ))} </tbody> </table> </> ); }; export default Day;
14. PUT(수정), DELETE(삭제)
Word.js
import React from 'react'; import { useState } from 'react'; const 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() { fetch(`http://localhost:3001/words/${word.id}`, { method: "PUT", headers: { "Content-Type": "application/json; charset=utf-8" }, 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 key={word.id} 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> ); }; export default Word;
15.POST(생성), useNavigate()
useHistory =>eact-router-dom@6 이상 사용안함 = > useNavigate 으로 변경할것
App.js
import { BrowserRouter, Routes, Route } from 'react-router-dom'; import Day from './component/Day'; import DayList from './component/DayList'; import EmptyPage from './component/EmptyPage'; import Header from './component/Header'; import CreateWord from './component/CreateWord'; import CreateDay from './component/CreateDay'; function App() { return ( <BrowserRouter> <div className="App"> <Header /> <Routes> <Route 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;
CreateWord.js
import { useRef } from 'react'; import useFetch from './../hooks/useFetch'; import { useNavigate } from 'react-router-dom'; const CreateWord = () => { const engRef = useRef(null); const korRef = useRef(null); const dayRef = useRef(null); const days = useFetch("http://localhost:3001/days"); const navigate = useNavigate(); //useHistory =>eact-router-dom@6 이상 사용안함 = > useNavigate 으로 변경할것 //const history = useHistory(); function onSubmit(e) { e.preventDefault(); // console.log("dayRef :", dayRef.current.value); // console.log("engRef :", engRef.current.value); // console.log("kor :", e.target.kor.value); const eng = engRef.current; const kor = e.target.kor; if (!eng.value) { alert("eng 를 입력해 주세요."); eng.focus(); return; } if (!kor.value) { alert("kor 를 입력해 주세요."); kor.focus(); return; } fetch(`http://localhost:3001/words`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ day: dayRef.current.value, eng: eng.value, kor: kor.value, isDone: false }), }) .then(res => { if (res.ok) { alert("생성이 완료 되었습니다."); // engRef.current.value = ""; // e.target.kor.value = ""; // dayRef.current.value = 1; navigate(`/day/${dayRef.current.value}`); } }); } return ( <form onSubmit={onSubmit}> <div className="input_area"> <label>Eng</label> <input type="text" name="eng" placeholder="cumputer" ref={engRef} /> </div> <div className="input_area"> <label>Kor</label> <input type="text" name="kor" placeholder="컴퓨터" ref={korRef} /> </div> <div className="input_area"> <label>Day</label> <select name="day" ref={dayRef} > {days.map(day => ( <option key={day.id} value={day.day}>{day.day}</option> ))} </select> </div> <button>저장</button> </form> ); }; export default CreateWord;
CreateDay.js
import { useNavigate } from 'react-router-dom'; import useFetch from './../hooks/useFetch'; const CreateDay = () => { const days = useFetch("http://localhost:3001/days"); const navigate = useNavigate(); function addDay(e) { e.preventDefault(); 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 > ); }; export default CreateDay;
16. 마치며
src/component/CreateWord.js
import { useRef } from 'react'; import useFetch from './../hooks/useFetch'; import { useNavigate } from 'react-router-dom'; import { useState } from 'react'; const CreateWord = () => { const engRef = useRef(null); const korRef = useRef(null); const dayRef = useRef(null); const [isLoading, setIsLoading] = useState(false); const days = useFetch("http://localhost:3001/days"); const navigate = useNavigate(); //useHistory =>eact-router-dom@6 이상 사용안함 = > useNavigate 으로 변경할것 //const history = useHistory(); function onSubmit(e) { e.preventDefault(); if (!isLoading) { const eng = engRef.current; const kor = e.target.kor; if (!eng.value) { alert("eng 를 입력해 주세요."); eng.focus(); return; } if (!kor.value) { alert("kor 를 입력해 주세요."); kor.focus(); return; } setIsLoading(true); fetch(`http://localhost:3001/words`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ day: dayRef.current.value, eng: eng.value, kor: kor.value, isDone: false }), }) .then(res => { if (res.ok) { alert("생성이 완료 되었습니다."); engRef.current.value = ""; e.target.kor.value = ""; //dayRef.current.value = 1; // navigate(`/day/${dayRef.current.value}`); setIsLoading(false); } }); } } return ( <form onSubmit={onSubmit}> <div className="input_area"> <label>Eng</label> <input type="text" name="eng" placeholder="cumputer" ref={engRef} /> </div> <div className="input_area"> <label>Kor</label> <input type="text" name="kor" placeholder="컴퓨터" ref={korRef} /> </div> <div className="input_area"> <label>Day</label> <select name="day" 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> ); }; export default CreateWord;
src/component/Day.js
import { useParams, useNavigate } from 'react-router-dom'; import Word from './Word'; import useFetch from './../hooks/useFetch'; const Day = () => { const { day } = useParams(); const days = useFetch(`http://localhost:3001/days`); const words = useFetch(`http://localhost:3001/words?day=${day}`); const navigate = useNavigate(); return ( <> <h2>Day {day}</h2> {words.length === 0 && <span>등록된 단어가 없습니다.</span>} <table> <tbody> {words.map(word => ( <Word key={word.id} word={word} /> ))} </tbody> </table> <div style={{ marginTop: "30px", display: "flex", justifyContent: "space-evenly" }}> <button onClick={() => { if (Number(day) > 1) { navigate(`/day/${Number(day) - 1}`) } }} style={{ opacity: Number(day) === 1 ? 0.3 : 1 }} >이전</button> <button onClick={() => { navigate(`/day/${Number(day) + 1}`) }} style={{ opacity: Number(day) === (days.length) ? 0.3 : 1 }} >다음</button> </div> </> ); };
src/component/DayList.js
if (days.length === 0) { return <div className='d-flex justify-content-center loading-spinner' style={{ marginTop: "50px", textAlign: "center" }} > <div className="spinner-border text-danger" style={{ width: '3rem', height: '3rem' }} role="status" > <span className="sr-only">Loading...</span> </div> </div > }
src/component/Word.js
17. 부록 : 타입스크립트를 적용해보자!
리액트 타입스크립트 설치
#npm npm install typescript @types/node @types/react @types/react-dom @types/jest @types/react-router-dom #yarn yarn add typescript @types/node @types/react @types/react-dom @types/jest @types/react-router-dom
info Direct dependencies
├─ @types/jest@28.1.8
├─ @types/node@18.7.13
├─ @types/react-dom@18.0.6
├─ @types/react-router-dom@5.3.3
├─ @types/react@18.0.17
└─ typescript@4.8.2
info All dependencies
├─ @types/jest@28.1.8
├─ @types/node@18.7.13
├─ @types/react-dom@18.0.6
├─ @types/react-router-dom@5.3.3
├─ @types/react-router@5.1.18
├─ @types/react@18.0.17
└─ typescript@4.8.2
Done in 10.65s.
* 환경설정 파일 변경
1. jsconfig.json 파일 -> tsconfig.json 파일명 및 내용 변경
2.. jsx &. js 확장자 파일 ->. tsx 파일로 확장자 변경
- tsconfig.json 기본 설정 내용
tsconfig.json 기본 설정 내용
{ "compilerOptions": { "target": "es6", "lib": [ "dom", "dom.iterable", "esnext" ], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "noFallthroughCasesInSwitch": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, "jsx": "react-jsx" }, "include": [ "src" ] }
타입스크립트로 변경
Word.tsx
~ interface IProps{ word: IWord, } export interface IWord{ day?: string, eng?:string, kor?: string, isDone?: boolean, id:number } const Word = ({word: w }:IProps) => { ~~ ~~ function del() { if (window.confirm("삭제 하시겠습니까?")) { fetch(`http://localhost:3001/words/${word.id}`, { method: "DELETE", }).then((res) => { if (res.ok) { setWord({ ...word, id:0 }); } }); } } ~~
Day.tsx
import { useParams, useNavigate } from 'react-router-dom'; import Word, { IWord } from './Word'; import useFetch from '../hooks/useFetch'; const Day = () => { const { day } = useParams<{day:string}>(); const days = useFetch(`http://localhost:3001/days`); const words :IWord[] = useFetch(`http://localhost:3001/words?day=${day}`); ~~ ~
DayList.tsx
~ interface IDay { id: 3, day: 3 } const DayList = () => { const days :IDay[] = useFetch("http://localhost:3001/days"); ~
CreateWord.tsx
import { useRef } from 'react'; import useFetch from '../hooks/useFetch'; import { useNavigate } from 'react-router-dom'; import { useState } from 'react'; import { IDay } from './DayList'; const CreateWord = () => { const engRef = useRef<HTMLInputElement>(null); const korRef = useRef<HTMLInputElement>(null); const dayRef = useRef<HTMLSelectElement>(null); const [isLoading, setIsLoading] = useState(false); const days :IDay[] = useFetch("http://localhost:3001/days"); const navigate = useNavigate(); //useHistory =>eact-router-dom@6 이상 사용안함 = > useNavigate 으로 변경할것 //const history = useHistory(); function onSubmit(e : React.FormEvent) { e.preventDefault(); if (!isLoading && dayRef.current && engRef.current && korRef.current) { const eng = engRef.current; const kor = korRef.current; const day = dayRef.current.value; ~~ ~
useFetch.ts
import { useState, useEffect } from 'react'; const useFetch = (url:string) => { const [data, setData] = useState([]); ~
소스 : https://github.com/braverokmc79/React-for-beginners
댓글 ( 4)
댓글 남기기