연습용으로 만든 Dummy데이터는 고정되었기에 사용자의 액션에 따라서 데이터를 읽고 쓰고 업데이트, 삭제해보자!
그러기 위해서는 DB를 구축하고 API를 만들어야하는데, JSON서버를 이용해서 RESTful API를 만들어 보자!
[REST API란?]
uri주소와 메서드로 CRUD요청을 하는 것!
Create : POST
Read : GET
Update : PUT
Delete : DELETE
URI는 식별하고, URL은 위치를 가르킨다.
URI: 웹 기술에서 사용하는 논리적 또는 물리적 리소스를 식별하는 고유한 문자열 시퀀스
URL: 흔히 웹 주소라고도 하며, 네트워크 상에 리소스가 어디에 있는지 알려주기 위한 규약
JSON서버는 빠르고 쉽게 REST API를 구축해준다.
공부 목적, 작은 프로젝트로 사용 가능하다.
[셋팅하기]
1. npm 설치
npm install -g json-server
json-server --watch ./src/db/data.json --port 3001
json-server --watch (경로) --띄울 포트넘버
[JSON 데이터 처리]
1. dummyList를 API통신해서 올바른 응답받기
useEffect()
첫번째 매개변수: 함수
두번째 매개변수: 의존성배열
어떤 상태값이 바뀌었을 때 동작하는 함수를 작성할 수 있다.
함수가 호출된 시기는 렌더링 결과가 실제 dom에 반영됐을때이다.
그리고 컴포넌트가 사라지기 직전에 마지막으로 호출된다.
상태값 변경 -> useEffect 함수 호출
렌더링이 끝나고 작업을 하고 싶으면 함수를 전달해주면 되는데,
매번 변경이 될때마다 불필요하게 함수가 호출 될 수 있다.
그럴 때 두번째 매개변수를 넣어준다.
//DayList.jsx
//리스트 출력
import React, { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
// import dummy from '../db/data.json'
function DayList() {
const [days, setDays] = useState([])
useEffect(() => {
console.log('111')
fetch('http://localhost:3002/days')//response는 http응답 실제 json이아님
.then(res => {
return res.json() //json파일로 변환됨
})
.then(data => {
setDays(data)
})
}, [])
return (
<div>
<ul className="list_day">
{
days.map(dum => (
<li
key={dum.id}
>
<Link to={`/day/${dum.day}`}>Day {dum.day}</Link>
</li>
))
}
</ul>
</div >
)
}
export default DayList
JSON.stringify() → 자바스크립트를 제이슨으로 변환
.json() → 제이슨 바디를 자바스크립트로 변환
//Day.jsx
// 특정 날짜를 클릭했을 때 단어가 나오게 만드는 페이지
import React, { useEffect, useState } from 'react'
import dummy from '../db/data.json'
import { useParams } from "react-router-dom"
import Word from './Word'
function Day() {
// day가 1인것만 나올 수 있도록 만들기
const { day } = useParams()
// json서버에서 데이터 통신
const [words, setWords] = useState([])
useEffect(() => {
fetch(`http://localhost:3002/words?day=${day}`)
.then(res => { return res.json() })
.then(data => setWords(data))
}, [day])
return (
<div>
<h2>Day {day}</h2>
{/* 단어를 클릭했을 때 실행되는 함수, day.id와 list.id 값을 비교해서 맞다면 출력 */}
<table>
<tbody>
{
words.map(word =>
<Word word={word} key={word.id} />
)
}
</tbody>
</table>
</div>
)
}
export default Day
.then(data => setWords(data.filter(db => db.day === Number(day))))
fetch(
http://localhost:3002/words?day=${day}`)`이렇게해도 동작은 된다.
더 간편한 방법은 서버주소를 백틱으로 감싸서 직접 쿼리스트링과 함께 추가한다.
[겹치는 부분]
동일한 로직은 custom Hooks를 사용해서 반복되는 로직을 사용할 수 있다.
//useFetch.jsx
import React, { useEffect, useState } from 'react'
function useFetch(url) {
const [data, setData] = useState([])
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(data => setData(data))
}, [url])
return data
}
export default useFetch
[api로 데이터를 받아오는 부분은 한줄로 처리가 가능해졌다.]
//DayList.js
import { Link } from 'react-router-dom'
import useFetch from '../hooks/useFetch'
function DayList() {
const days = useFetch("http://localhost:3002/days")
return (
<div>
<ul className="list_day">
{
days.map(dum => (
<li
key={dum.id}
>
<Link to={`/day/${dum.day}`}>Day {dum.day}</Link>
</li>
))
}
</ul>
</div >
)
}
export default DayList
PUT method를 이용해서 isDone을 수정해보자.
현재는 isDone을 단순히 true / false가 되어있어서 새로고침시에 모습이 유지되지 않는데, put을 이용해서 새로고침시에도 작동한 그대로 남아있을 수 있게 데이터를 보내준다.
//Word.jsx
const toggleDone = () => {
// setIsDone(!isDone)
fetch(`http://localhost:3001/words/${word.id}`, {
method: "PUT", //2. 요청의 옵션을 입력한다.
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...word,
isDone: !isDone,
}),
}).then(res => {
if (res.ok) {
setIsDone(!isDone)
}
})
}
두번째 인자로 객체를 받아온 후 해당 객체안에 요청의 옵션들을 입력한다.
import React, { useState } from 'react'
function Word({ word }) {
...
// delete
function del() {
if (window.confirm('삭제 하시겠습니까?')) {
fetch(`http://localhost:3001/words/${word.id}`, {
method: 'DELETE',
})
}
}
return (
<div>
<tr className={isDone ? 'off' : ''}>
<td>
<input
type="checkbox"
// checked={word.isDone}
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>
</div>
)
}
export default Word
동작하는 화면을 확인했더니..
confirm창이 뜨지만 아무 변화가 없는상태였고, 새로고침하니 삭제가 된 화면이 출력되는것을 확인할 수 있다.
실제단어는 지워지지만, 페이지에는 변화가 없는데 삭제된 이후의 단어리스트를 다시 그려주지 않아서 그렇다.
삭제요청 -> 확인 -> 컴포넌트 리렌더링 과정이 필요하다.
이때 null을 리턴해주면 아무것도 표현해주지 않는다.
그러면 data의 모습을 null로 바꾸면 클릭시 브라우저에서 삭제된 상태로 표시가 된다.
function Word({ word: w }) {
const [word, setWord] = useState(w) //w라는 새로운 변수명으로 할당
...
...
// delete
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
}
word를 state로 만들고 props로 받아오는데, 새로운 변수명으로 받아올 수 있다.
삭제가 되면, word의 id를 0으로 바꿔준다.
word의 id가 0일경우 null을 리턴해준다. ( id가 0인상태이면 해당 데이터는 기존 데이터를 날려버리고 null이 차지하게 됨 )
알고 넘어갈 부분
state를 props로 받아 왔는데, 이름이 겹쳤던 상황이였다.
function Word(props) {
const [word, setWord] = useState(props.word)
...
또는
function Word({ word: w }) {
const [word, setWord] = useState(w)
...
새로운 변수명으로 받아옴 => 구조 분해 할당을 이용했음
새로운 페이지에서 단어 추가와 day 추가 기능을 만들어 보자.
CreateWord.jsx를 하나 만들어주고 App.js에서는 route를 해주고
header에서 Link걸어주기.
import React from 'react'
import useFetch from '../hooks/useFetch'
export default function CreateWord() {
const addWord = useFetch("http://localhost:3003/days")
const handleSubmi = (e) => {
e.preventDefault()
}
return (
<div>
<form action="submit">
<div>
<label for="eng">영어 단어</label>
<input type="text" id="eng" />
</div>
<div>
<label for="kor">단어 뜻</label>
<input type="text" id="kor" />
</div>
<div>
<label for="day">추가</label>
<select name="days" id="day">
{
addWord.map(word => (
<option key={word.id} value={word.day}>day {word.day}</option>
))
}
</select>
</div>
</form>
<button onClick={handleSubmi}>
저장
</button>
</div >
)
}
useFetch로 만든 useEffect를 가져와서 map을 돌려 days를 select리스트로 만든다.
//createWord.jsx
import React, { useRef } from 'react'
import useFetch from '../hooks/useFetch'
export default function CreateWord() {
const addWord = useFetch("http://localhost:3003/days")
const handleSubmi = (e) => {
e.preventDefault()
console.log(engRef.current.value)
console.log(korRef.current.value)
console.log(dayRef.current.value)
}
const engRef = useRef(null)
const korRef = useRef(null)
const dayRef = useRef(null)
return (
<div>
<form action="submit">
<div>
<label for="eng">영어 단어</label>
<input type="text" id="eng" ref={engRef} />
</div>
<div>
<label for="kor">단어 뜻</label>
<input type="text" id="kor" ref={korRef} />
</div>
<div>
<label for="day">추가</label>
<select name="days" id="day" ref={dayRef}>
{
addWord.map(word => (
<option key={word.id} value={word.day}>day {word.day}</option>
))
}
</select>
</div>
</form>
<button onClick={handleSubmi}>
저장
</button>
</div >
)
}
const engRef = useRef(null) const korRef = useRef(null) const dayRef = useRef(null)
<input type="text" id="kor" ref={korRef} />
이렇게 연결해주면 DOM요소가 생성된 후 접근할 수 있다.
위 코드에서 저장 버튼을 클릭하는 시점은 렌더링 결과가 돔에 반영된 후이다.
engRef.current.value
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생성
alert('생성이 완료됐습니다.')
}
})
axios.post('http://localhost:3003/words', {
day: dayRef.current.value,
kor: korRef.current.value,
eng: engRef.current.value,
isDone: false
}).then((res) => console.log(res))
}
양식이 제출되었거나 특정 event발생시 url을 조작해줄 수 있다.
const history = useNavigate()
...
axios.post('http://localhost:3003/words', {
day: dayRef.current.value,
kor: korRef.current.value,
eng: engRef.current.value,
isDone: false
}).then(() =>
alert('등록이 완료됐습니다.'),
history(`/day/${dayRef.current.value}`)
)
[완성된 createWord.jsx]
import { useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import axios from 'axios'
import useFetch from '../hooks/useFetch'
export default function CreateWord() {
const addWord = useFetch("http://localhost:3003/days")
const history = useNavigate()
const engRef = useRef(null)
const korRef = useRef(null)
const dayRef = useRef(null)
const handleSubmi = (e) => {
e.preventDefault()
// console.log(engRef.current.value)
// console.log(korRef.current.value)
// console.log(dayRef.current.value)
axios.post('http://localhost:3003/words', {
day: dayRef.current.value,
kor: korRef.current.value,
eng: engRef.current.value,
isDone: false
}).then(() =>
alert('등록이 완료됐습니다.'),
history(`/day/${dayRef.current.value}`)
)
}
return (
<div>
<form action="submit">
<div>
<label for="eng">영어 단어</label>
<input type="text" id="eng" ref={engRef} />
</div>
<div>
<label for="kor">단어 뜻</label>
<input type="text" id="kor" ref={korRef} />
</div>
<div>
<label for="day">추가</label>
<select name="days" id="day" ref={dayRef}>
{
addWord.map(word => (
<option key={word.id} value={word.day}>day {word.day}</option>
))
}
</select>
</div>
</form>
<button onClick={handleSubmi}>
저장
</button>
</div >
)
}
[createDay도 만들어 보자]
import axios from 'axios'
import React from 'react'
import { useNavigate } from 'react-router-dom'
import useFetch from '../hooks/useFetch'
export default function CreateDay() {
const newPage = useNavigate('')
const days = useFetch('http://localhost:3003/days')
let daysNum = days.length
const addDay = () => {
axios.post('http://localhost:3003/days', {
day: daysNum + 1
}).then(
alert('등록되었습니다.'),
newPage(`/`)
)
}
return (
<div>
<h2>현재 일수: {days.length}</h2>
<div>
<button onClick={addDay}>day 추가</button>
</div>
</div>
)
}