[React] 초보자를 위한 리액트 강좌 3
더미 데이터 구현, map() 반복문
db폴더 생성 - data.json 파일 생성
데이터 - https://github.com/coding-angma/voca/blob/lesson/9/src/db/data.json
@ DayList 컴포넌트 생성
day 개수만큼 <li>를 만들어줘야 함 -> map 사용
import dummy from "../db/data.json";
export default function DayList() {
console.log(dummy);
return (
<ul className="list_day">
{dummy.days.map((day) => (
<li>Day {day.day}</li>
))}
</ul>
);
}
이 상태에서 console을 보면 경고문이 뜸
Warning: Each child in a list should have a unique "key" prop.
반복되는 요소에 고유한 값을 넣어주는 용도로 key 사용
<li key={day.id}>Day {day.day}</li>
* 리액트에서 key가 필요한 이유
https://www.youtube.com/watch?v=ghxHAy3LH28
리액트에서 key는 요소의 변화를 알아차리기 위해 필요
@ Day 컴포넌트 생성
import dummy from "../db/data.json";
export default function Day() {
return (
<>
<table>
<tbody>
{dummy.words.map((word) => (
<tr>
<td>{word.eng}</td>
<td>{word.kor}</td>
</tr>
))}
</tbody>
</table>
</>
);
}
import Day from "./component/Day";
import DayList from "./component/DayList";
import Header from "./component/Header";
function App() {
return (
<div className="App">
<Header />
<DayList />
<Day />
</div>
);
}
export default App;
문제점1: 모든 단어가 다 나오고 있음
@ filter를 사용해서 Day를 고정 (ex. Day1)
import dummy from "../db/data.json";
export default function Day() {
// dummy.words
const day = 1;
const wordList = dummy.words.filter((word) => word.day === day);
console.log(wordList);
return (
<>
<table>
<tbody>
{wordList.map((word) => (
<tr key={word.id}>
<td>{word.eng}</td>
<td>{word.kor}</td>
</tr>
))}
</tbody>
</table>
</>
);
}
문제점2: DayList 밑에 단어가 뜨는 게 아니라 Day를 클릭했을 때 다른 페이지에서 단어가 떠야 함
-> 다른 페이지를 만들어보자
라우터 구현
react-router-dom 버전 v5로 구현하는 코드 (최신 버전 v6)
v6 삭제 : npm uninstall react-router-dom
v5 설치 : npm install react-router-dom@5.2.0
리액트 라우터 돔 설치
npm install react-router-dom
1. import 후 BrouserRouter로 App 전체를 감쌈
2. Header는 모든 페이지에 나와야 하니까 Header 다음 부분을 Switch로 감쌈
-> Switch 내부는 url에 따라 각기 다른 페이지를 보여줌
-> Switch 외부는 모든 페이지에 공통으로 노출
4. path는 주소
-> "/" = 첫 페이지를 의미
-> <Route path="/">와 <Route path="/day">가 있다면 두 번째 코드에 "/"가 포함되어 있어 첫 페이지로 이동되므로 <Route exact path="/"> 이런식으로 작성
import Day from "./component/Day";
import DayList from "./component/DayList";
import Header from "./component/Header";
import { BrowserRouter, Route, Switch } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<div className="App">
<Header />
<Switch>
<Route exact path="/">
<DayList />
</Route>
<Route path="/day">
<Day />
</Route>
</Switch>
</div>
</BrowserRouter>
);
}
export default App;
HTML은 a태그를 사용하지만 리액트 라우터는 Link를 사용
@ Day 클릭시 단어들 리스트로 넘어가기
// DayList.js
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>
);
}
@ 페이지 제목 클릭시 첫 번째 페이지로 넘어가기
// Header.js
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>
);
}
리액트에서 다이나믹한 URL을 처리할 때는 ":"으로 처리
// App.js
...
<Route path="/day/:day">
...
만일 http://localhost:3000/day/1 이런식의 URL로 들어왔다면 day라는 변수로 1의 값을 얻을 수 있음
"/day/:id"였다면 id라는 변수로 1의 값을 얻을 수 있는 것
URL에 포함된 값을 얻을 때는 리액트 라우터 돔에서 제공하는 useParams 훅을 사용
주소가 http://localhost:3000/day/3과 같을 때
const a = useParams();
console.log(a);
콘솔을 보면 {day: '3'}이런식으로 찍힘
<Route path="/day/:id">였다면 {id: '3'}으로 나오게 됨
@ 특정 날짜로 이동
// Days.js
...
export default function Day() {
// (1)
const a = useParams();
const day = a.day;
// (2)
const day = useParams().day;
// (3)
const { day } = useParams();
const wordList = dummy.words.filter((word) => word.day === day);
...
단어들은 안나오는 상태 -> 문자열과 숫자를 비교하고 있기 때문
word.day는 숫자인 반면 day는 문자열
-> Number(day)로 바꿔주자
@ 없는 주소 처리
// EmptyPage.js
import { Link } from "react-router-dom";
export default function Day() {
return (
<>
<h2>잘못된 접근입니다.</h2>
<Link to="/">돌아가기</Link>
</>
);
}
// App.js
import Day from "./component/Day";
import DayList from "./component/DayList";
import Header from "./component/Header";
import EmptyPage from "./component/EmptyPage";
import { BrowserRouter, Route, Switch } from "react-router-dom";
function App() {
return (
<BrowserRouter>
<div className="App">
<Header />
<Switch>
<Route exact path="/">
<DayList />
</Route>
<Route path="/day/:day">
<Day />
</Route>
<Route>
<EmptyPage />
</Route>
</Switch>
</div>
</BrowserRouter>
);
}
export default App;
Route에 path를 적어주지 않고 제일 밑에 적어줌
앞에 있는 조건이 모두 만족하지 않으면 EmptyPage가 보여지게 됨
만일 제일 위에 적어두면 모든 페이지가 EmptyPage로 가게 됨
json-server, REST API
@ 체크박스, 뜻 보기, 삭제 버튼 추가
// Days.js
import dummy from "../db/data.json";
import { useParams } from "react-router-dom";
export default function Day() {
const { day } = useParams();
const wordList = dummy.words.filter((word) => word.day === Number(day));
console.log(wordList);
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{wordList.map((word) => (
<tr key={word.id}>
<td>
<input type="checkbox" />
</td>
<td>{word.eng}</td>
<td>{word.kor}</td>
<td>
<button>뜻 보기</button>
<button className="btn_del">삭제</button>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}
@ 뜻 보기 작업 - 별도의 컴포넌트에서 수행
: 각 컴포넌트별로 state를 가짐 -> 뜻 보기를 눌렀을 때 뜻이 나타났다 없어졌다 하는 건 단어 하나에만 해당하는 거이기 때문에 따로 컴포넌트를 제작해주는 것이 좋음
// Word.js
export default function Word({ word }) {
return (
<tr>
<td>
<input type="checkbox" />
</td>
<td>{word.eng}</td>
<td>{word.kor}</td>
<td>
<button>뜻 보기</button>
<button className="btn_del">삭제</button>
</td>
</tr>
);
}
// Day.js
import dummy from "../db/data.json";
import { useParams } from "react-router-dom";
import Word from "./Word";
export default function Day() {
const { day } = useParams();
const wordList = dummy.words.filter((word) => word.day === Number(day));
console.log(wordList);
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{wordList.map((word) => (
<Word word={word} key={word.ud} />
))}
</tbody>
</table>
</>
);
}
@ 뜻 보기/숨기기 기능 구현
import { useState } from "react";
export default function Word({ word }) {
const [isShow, setIsShow] = useState(false);
function toggleShow() {
setIsShow(!isShow);
}
return (
<tr>
<td>
<input type="checkbox" />
</td>
<td>{word.eng}</td>
<td>{isShow && word.kor}</td>
<td>
<button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
<button className="btn_del">삭제</button>
</td>
</tr>
);
}
@ 외운 단어 체크, 못 외운 단어 미체크
import { useState } from "react";
export default function Word({ word }) {
const [isShow, setIsShow] = useState(false);
function toggleShow() {
setIsShow(!isShow);
}
return (
<tr className={word.isDone ? "off" : ""}>
<td>
<input type="checkbox" checked={word.isDone} />
</td>
<td>{word.eng}</td>
<td>{isShow && word.kor}</td>
<td>
<button onClick={toggleShow}>뜻 {isShow ? "숨기기" : "보기"}</button>
<button className="btn_del">삭제</button>
</td>
</tr>
);
}
이렇게 구현했을 시 콘솔창을 보면 아래와 같은 경고 문구가 보임
Warning: You provided a `checked` prop to a form field without an `onChange` handler. This will render a read-only field. If the field should be mutable use `defaultChecked`. Otherwise, set either `onChange` or `readOnly` .
어떤 액션을 취하더라도 checked가 값이 고정되어 있으므로 읽기 전용이랑 다를 바가 없다는 뜻
-> onChange를 추가해서 반응형으로 만들기
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() {
setIsShow(!isDone);
}
return (
<tr className={isDone ? "off" : ""}>
<td>
<input type="checkbox" checked={word.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>
);
}
@ 사용자 액션에 따라 데이터 CRUD
-> DB구축, API를 만들어야 함 (json-server를 이용해서 REST API를 만들기)
json-server는 빠르고 쉽게 REST API를 구축해줌
npm install -g json-server
json-server --watch ./src/db/data.json --port 3001
이제 localhost:3001/days,localhost:3001/words로 가보면 데이터들이 보임
REST API란?
URI주소와 메소드로 CRUD요청을 하는 것
Create : Post
Read : GET
Update : PUT
Delete : DELETE
useEffect, fetch()로 API 호출
useEffect는 어떤 상태값이 바뀌었을 때 동작하는 함수를 작성할 수 있는 훅
첫 번째 매개변수로 함수를 넣음 : 이 함수가 호출 된 타이밍은 렌더링 결과가 실제 돔에 반영된 직후, 그리고 컴포넌트가 사라지기 직전에도 마지막으로 호출
@ 연습
// DayList.js
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>
</>
);
}
위와 같이 코드를 작성했을 때 Day Change버튼을 누르면 count는 변경되지 않았음에도 불구하고 console에 Count Change가 찍히는 것을 볼 수 있음
= useEffect의 함수가 불필요하게 호출되고 있음
-> useEffect의 두 번째 매개변수로 배열 넣기
useEffect(() => {
console.log("Count change");
}, [count]);
이런식으로 작성을 해주면 count가 변경이 되었을 때만 함수가 실행 됨
이런 것을 의존성 배열이라고 함
의존성 배열의 값이 변경되는 경우에만 함수가 실행
useEffect를 여기서 사용하는 목적은 API호출
렌더링이 완료된 후 최초에 한 번만 API를 호출해주면 됨
이런 경우에는 의존성 배열을 빈 배열로 입력함
count를 누르고 Day Change를 계속 눌러도 Count change는 한 번만 찍힘
이렇게 상태값과 무관하게 렌더링 직후 딱 한 번만 실행되는 작업은 빈 배열을 전달하면 됨
@ API 호출
API 비동기 통신을 위해서 fetch를 이용
useEffect(() => {
fetch("http://localhost:3001/days") // API 경로, 프로미스가 반환
.then((res) => { // response는 http응답이고 실제 json은 아님
return res.json(); // 그래서 json메소드를 사용해서 json으로 변환
})
.then((data) => {
setDays(data);
});
}, []);
네트워크 탭을 확인해보면 days호출이 잘 된 것을 볼 수 있음
@ dummy데이터 사용했던 부분 수정
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import Word from "./Word";
export default function Day() {
const { day } = useParams();
const [words, setWords] = useState([]);
useEffect(() => {
fetch(`http://localhost:3001/words?day=${day}`)
.then((res) => {
return res.json();
})
.then((data) => {
setWords(data);
});
}, []); // (1)
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{words.map((word) => (
<Word word={word} key={word.ud} />
))}
</tbody>
</table>
</>
);
}
그런데 이렇게 코드를 작성하면 (1)부분에서 day가 의존성 배열에 없다는 경고가 뜨게 됨
: useEffect내부에서 day와 같이 특정값을 사용하면 의존성 배열에 입력하라는 뜻
의존성 배열이 비어있으면 day가 변경되어도 새로운 정보를 가져올 수가 없음
의존성 배열에 day를 넣어주게 되면 이 값이 최신값이라고 보장받을 수 있음
아래와 같이 의존성 배열에 day를 넣어주자
useEffect(() => {
fetch(`http://localhost:3001/words?day=${day}`)
.then((res) => {
return res.json();
})
.then((data) => {
setWords(data);
});
}, [day]);
그런데 DayList와 Day가 상당히 비슷함
주소를 제외하고 state를 만들고 fetch로 리스트를 가져와서 set을 해준다는 동작들은 동일함
동일한 로직은 사용자가 직접 훅을 만들어서 사용할 수 있음
= Custom Hook
Custom Hooks
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;
}
API주소를 url로 넘겨받아서 fetch를 해주고 의존성 배열도 바뀔 수 있으므로 url을 입력해줌
필요로 하는 것은 data이므로 return
@ 적용
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 Word from "./Word";
export default function Day() {
const { day } = useParams();
const words = useFetch(`http://localhost:3001/words?day=${day}`);
return (
<>
<h2>Day {day}</h2>
<table>
<tbody>
{words.map((word) => (
<Word word={word} key={word.ud} />
))}
</tbody>
</table>
</>
);
}
📌 아래 강의의 내용을 정리한 글입니다.