리액트 CRUD 해보기

mangorovski·2022년 10월 13일
2

연습용으로 만든 Dummy데이터는 고정되었기에 사용자의 액션에 따라서 데이터를 읽고 쓰고 업데이트, 삭제해보자!

그러기 위해서는 DB를 구축하고 API를 만들어야하는데, JSON서버를 이용해서 RESTful API를 만들어 보자!

[REST API란?]
uri주소와 메서드로 CRUD요청을 하는 것!
Create : POST
Read : GET
Update : PUT
Delete : DELETE

URI는 식별하고, URL은 위치를 가르킨다.
URI: 웹 기술에서 사용하는 논리적 또는 물리적 리소스를 식별하는 고유한 문자열 시퀀스
URL: 흔히 웹 주소라고도 하며, 네트워크 상에 리소스가 어디에 있는지 알려주기 위한 규약

  1. HTTP URI(Uniform Resource Identifier)을 통해 자원(Resource)를 명시하고
  2. HTTP Method(POST, GET, DELETE, PATCH)를 통해
  3. 해당 자원(URI)에 대한 CRUE Operation을 적용하는 것을 의미한다.

JSON서버는 빠르고 쉽게 REST API를 구축해준다.
공부 목적, 작은 프로젝트로 사용 가능하다.

[셋팅하기]
1. npm 설치

npm install -g json-server 
  1. 서버띄우기
json-server --watch ./src/db/data.json --port 3001
json-server --watch (경로) --띄울 포트넘버

Read

[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
  • 새로운 state를 생성한다. api를 이용해 list를 가져오고 setState를 통해 자동으로 렌더링하도록 만든다.
  • fetch에는 연결된 http에서 실제 응답해줄 server를 연결시킨다.
  • json data를 setState값으로 넣어준다.

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
  • json서버 fetch후에 useParams를 통해 가져온 day를 이용해 filter시킬까?
    .then(data => setWords(data.filter(db => db.day === Number(day))))

    fetch(http://localhost:3002/words?day=${day}`)`

이렇게해도 동작은 된다.
더 간편한 방법은 서버주소를 백틱으로 감싸서 직접 쿼리스트링과 함께 추가한다.

  • useEffect에서 특정값을 사용하게 되면 의존성되는 배열값을 넣어줘야하는데 위 코드에서 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
  • 서버주소를 parameter로 받아온다. 결국 필요로 하는것은 data return해주는 것이다.
  • data라는 상태값이 있고 URL이란 주소를 넘겨받아서 fetch시킴 -> 응답받은 데이터를 setState해주고 data를 리턴한다.

[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
  • 컴포넌트가 렌더링되면 useFetch안에 있는 useEffect가 실행된다.
  • return된 값을 변수에 넣어 리스트를 만들어준다.

update

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)
        }
    })
}

두번째 인자로 객체를 받아온 후 해당 객체안에 요청의 옵션들을 입력한다.

  • method는 "PUT"
  • Content-type은 보내는 리소스 타입을 말한다. 문자열, 이미지, html, json등 다양하게 보낼 수 있다.
  • body에는 단순히 가져오는 get과는 다르게 put은 수정을 위한 정보들을 실어서 보내줘야한다.
    body안에는 기존data(...word)에 isDone을 바꿔서 입력시키는데, JSON.stringify를 이용해서 json문자열로 변환시킨다.

delete

  • 기능구현: 삭제버튼을 누르면 comfirm창을 띄우고 삭제를 진행시킨다.
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
  • del이라는 함수를 하나 만들고 method에 'DELETE'를 시켜준다.
  • 해당 함수는 '삭제'버튼을 클릭 시 동작할 수 있게 한다.

동작하는 화면을 확인했더니..
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) 
    ...

새로운 변수명으로 받아옴 => 구조 분해 할당을 이용했음


POST

새로운 페이지에서 단어 추가와 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리스트로 만든다.

useRef()

  • useRef 로 특정 DOM 선택하기
    아래 코드에서는 input창에 적은 값을 얻기위해서 useRef를 사용한다.
    dom에 접근할때 사용하는데, 스크롤 위치나 포커스를 줄 때 사용할 수 있다.
//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 >
    )
}
  • 먼저 useRef() 를 사용하여 Ref 객체를 만든다.
    const engRef = useRef(null) const korRef = useRef(null) const dayRef = useRef(null)
  • 선택하고 싶은 DOM에 ref를 연결해준다.
    <input type="text" id="kor" ref={korRef} />

이렇게 연결해주면 DOM요소가 생성된 후 접근할 수 있다.
위 코드에서 저장 버튼을 클릭하는 시점은 렌더링 결과가 돔에 반영된 후이다.

  • current속성을 이용하면 원하는 dom요소에 접근할 수 있다.
    engRef.current.value
    해당 코드에서는 current.value를 했는데, 이렇게 하면 input에 입력된 value값을 얻을 수 있다.

post를 이용해서 단어 생성하기

  1. fetch() 사용
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('생성이 완료됐습니다.')
    }
})
  1. axios 사용
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))
}

useNavigate()

  • Link 는 특정 주소로 이동해주는 태그였다면,
    Navigate 는 특정 행동을 했을 때 해당 주소로 이동해줄 수 있게 만들어준다.

양식이 제출되었거나 특정 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>
    )
}
profile
비니로그 쳌킨

0개의 댓글