3 Client - REST API 통신

source: categories/study/react_restapi_graphql/react_restapi_graphql4.md

3.1 REST API 통신 기능 구현

위와 같이


yarn run server
yarn run client

서버와 클라이언트 둘 다 실행합니다.

이 상태에서 axios를 불러오기위해서 기본 작업을 해놓겠습니다.


root_folder/
|-- client/
|   |-- components/
|       `-- MsgInput.js
|       `-- MsgItem.js
|       `-- MsgList.js
|   |-- pages/
|       `-- _app.js
|       `-- index.js
|       `-- index.scss
|   `-- fetcher.js
|   `-- next.config.js
|   `-- package.json
|-- server/
|   |-- src/
|       |-- db/
|           `-- messages.json
|           `-- users.json
|       |-- routes/
|           `-- messages.js
|           `-- users.js
|       `-- dbController.js
|       `-- index.js
|   `-- nodemon.json
|   `-- package.json
|-- package.json


// client/fetcher.js
import axios from "axios";

// axios의 defaults의 baseURL에 'http://localhost:8000' <- 서버 url주소를 넣겠습니다.
// 이를 지정을 해줘야 뒤에 route만 지정해줘도 잘 인식합니다.
// 그렇지 않을 경우엔 axios.get('http://localhost:8000', ...) 이런식으로 해야됩니다.
// 그런데 baseURL을 지정하고나면 axios.get('', ...) 이렇게 위 부분이 없어도됩니다.
axios.defaults.baseURL = 'http://localhost:8000'

// fetcher는 axios를 좀 더 편하게 쓰기위해서 만든것입니다.
const fetcher = async (method, url, ...rest) => {
    const res = await axios[method](url, ...rest)
    return res.data
}

// 위의 함수에서 3번째 인자인 ...rest의 의미는 다음과 같습니다.
/*
get: axios.get(url[, config]) // get과 delete는 첫번째 인자로 url을 받고 그 뒤에 옵션값 config를 받습니다.
delete: axios.delete(url[, config]) // config는 기타 설정에 대해 들어오는 부분이라고 보시면됩니다.
post: axios.post(url, data[, config]) // post나 put 같은 경우는 data를 update하거나 새로 create해야되기 때문에 새로운 값이 담긴 data가 반드시 와야합니다.
put: axios.put(url, data[, config]) // 그렇기 때문에 get이나 delete와 다르게 method의 파라미터로 들어가야할 인자값이 하나가 더 있는 것입니다.
 */
// 이런 2가지 경우를 모두 대비하기위해서 인자가 1개만 들어오거나 2개가 들어오거나 모두 처리할 수 있게끔 ...rest 인자로 설정해놓은 것입니다.

export default fetcher

클라이언트에서 GET 기능 구현 - 모든 메시지 다 가져오기 (Read)


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))




// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);
    const onCreate = text => {
        const newMsg = {
            id: msgs.length + 1,
            userId: getRandomUserId(),
            timestamp: Date.now(),
            text: `${msgs.length + 1} ${text}`,
        }
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    const onUpdate = (text, id) => {
        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, {
                ...msgs[targetIndex], // 기존 속성들을 받아오고,
                text // text만 새걸로 업데이트해주면된다.
            })
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    // useEffect 함수를 사용합니다. 두번째 인자로 빈배열을 넘겨줍니다. 최초 컴포넌트가 렌더링시 딱 한번만 실행되도록 하는 것입니다.
    useEffect(async () => {
        const msgs = await fetcher('get', '/messages') // fetcher는 async 함수. fetcher 내부만 await로 기다려서 비동기를 동기적으로 실행시키는거지
                                                                  // fetcher 자체는 비동기라 async, await 키워드를 이렇게 붙여준다.
        setMsgs(msgs)
    }, [])
    // 그런데 useEffect 내부에선 async, await를 직접 호출하지 않도록 하고있습니다.

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;



// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))




// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);
    const onCreate = text => {
        const newMsg = {
            id: msgs.length + 1,
            userId: getRandomUserId(),
            timestamp: Date.now(),
            text: `${msgs.length + 1} ${text}`,
        }
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    const onUpdate = (text, id) => {
        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, {
                ...msgs[targetIndex], // 기존 속성들을 받아오고,
                text // text만 새걸로 업데이트해주면된다.
            })
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;

이렇게 server에서 불러온 data가 네트워크 messages 요청을 하고 그 받아온 데이터를 화면에 뿌려주는 것까지 확인을 했습니다.

Note

위에보면 MsgList 컴포넌트에서 getMessages 함수가 useEffect라는 react hook을 통해 컴포넌트가 생성되는 시점에서 호출된다.
getMessages 함수가 호출되면서 fetcher 함수가 호출되고, 이 fetcher 함수는 axios를 통해 서버에 데이터 요청을 보낸다.
그러면 서버쪽.. server/src/routes/messages.js에 정의한 REST APIGET MESSAGES 라우터 부분이 실행된다.
모든 메시지 정보를 받아와 반환을하고,
이 값을 getMessages 함수에서 setMsgs라는 state 수정 함수로 수정한다.
state 값이 바뀌게되므로 해당 값으로 화면을 다시 렌더링하고, 메시지가 화면에 뿌려진다.

클라이언트에서 POST 기능 구현 (Create)

post를 위한 onCreate 메소드는 useEffect로 관리하는 메소드가 아니기 때문에 아래와 같이 onCreate 메소드에 바로 async를 적용할 수 있습니다.


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))




// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);
    
    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        // const newMsg = {
        //     id: msgs.length + 1,
        //     userId: getRandomUserId(),
        //     timestamp: Date.now(),
        //     text: `${msgs.length + 1} ${text}`,
        // }
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    const onUpdate = (text, id) => {
        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, {
                ...msgs[targetIndex], // 기존 속성들을 받아오고,
                text // text만 새걸로 업데이트해주면된다.
            })
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;

Note

MsgList 컴포넌트에서 정의한 onCreate 메소드가 호출되면, fetcher 함수를 통해 axios 라이브러리로 POST(CREATE) 요청을 보내게된다.
그러면 서버쪽.. server/src/routes/messages.js에서 정의한 REST APIpost 라우터가 실행된다.
넘겨받은 textuserId를 통해 newMsg를 만들고, 이를 unshift해서 데이터베이스에 저장한다.
그리고 newMsg를 반환한다.

그러면 onCreate안에서 newMsg를 받아 state값을 수정하는 setMsgs 함수를 통해 메시지 데이터를 수정한다.
state 값이 변경되었으므로 렌더링이 발생하고 화면에 새로운 데이터들이 다시 그려지게된다.


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))




// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);
    
    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        // const newMsg = {
        //     id: msgs.length + 1,
        //     userId: getRandomUserId(),
        //     timestamp: Date.now(),
        //     text: `${msgs.length + 1} ${text}`,
        // }
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    const onUpdate = (text, id) => {
        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, {
                ...msgs[targetIndex], // 기존 속성들을 받아오고,
                text // text만 새걸로 업데이트해주면된다.
            })
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.
    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    const onUpdate = (text, id) => {
        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, {
                ...msgs[targetIndex], // 기존 속성들을 받아오고,
                text // text만 새걸로 업데이트해주면된다.
            })
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;

이렇게 하면 됩니다.
확인을 해볼까요?

Note

위와 같이 urluserId 파라미터 값으로 jay가 설정되어있으므로 새글을 등록(Post(create))하면 jay라는 userId로 등록이된다.

Note

server/src/routes/message.js 파일의 post 라우터에서 인자값을 빼먹었습니다.
추가해주세요.

Note

위와 같이 urluserId 파라미터 값을 어떻게 설정하냐에따라 id가 다르게 설정되는 것을 볼 수 있습니다.


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.

    // 지금 userId를 받아온 현상태에서 MsgItem으로 넘겨주시면 MsgItem에서도 수정/삭제버튼 노출 여부를 결정할 수 있겠죠?

    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    const onUpdate = (text, id) => {
        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, {
                ...msgs[targetIndex], // 기존 속성들을 받아오고,
                text // text만 새걸로 업데이트해주면된다.
            })
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                                           myId={userId}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;


// client/components/MsgItem.js
import MsgInput from "./MsgInput";

const MsgItem = ({
    id,
    userId,
    timestamp,
    text,
    onUpdate,
    onDelete,
    isEditing,
    startEdit,
    myId,
                 }) => (
    <li className='messages__item'>
        <h3>
            {userId}{' '}
            <sub>
                {new Date(timestamp).toLocaleTimeString('ko-KR', {
                    year: 'numeric',
                    month: 'numeric',
                    day: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: true // 오전 오후 시간이 보여지도록 하는 설정
                })}
            </sub>
        </h3>
        {isEditing ? (
            <>
                <MsgInput mutate={onUpdate} text={text} id={id}/>
            </>
        ): text}

        {myId === userId &&
        <div className="messages__buttons">
            <button onClick={startEdit}>수정</button>
            <button onClick={onDelete}>삭제</button>
        </div>
        }
    </li>
)

export default MsgItem

위와 같이 수정하면 아래와 같이 접속한 userId에 따라 수정, 요청 버튼이 마우스 호버시 나타납니다.

클라이언트에서 PUT 기능 구현 (Update)


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.

    // 지금 userId를 받아온 현상태에서 MsgItem으로 넘겨주시면 MsgItem에서도 수정/삭제버튼 노출 여부를 결정할 수 있겠죠?

    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        if (!newMsg) throw Error('something wrong') // 여기도 안전 장치를 둡시다.
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    // 이번엔 update 기능(PUT)을 수정하겠습니다.
    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong') // 안전장치. newMsg가 없다면 아무것도 하지 않도록 설정

        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            // 아래 3번째 인자만 newMsg로 수정하면 됩니다.
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    const onDelete = (id) => {
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                                           myId={userId}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;


REST API update에 대한 상세설명


// MsgList 컴포넌트의 PUT(UPDATE) 부분입니다
// MsgList 컴포넌트에서 아래의 onUpdate 메소드가 실행되면 마찬가지로 fetcher 메소드가 실행됩니다.
// 인자 값으론 text와 id가 전달됩니다. 
    // text는 useRef로 textarea의 value를 참조한 값이고 
    // id는 각 메시지의 id(uuid의 v4() 함수로 생성된 id이다. (처음 생성한 메시지들의 id는 1~50까지의 숫자임)
const onUpdate = async (text, id) => {
    // fetcher 함수를 통해 axios 요청을 보냅니다.
        // put: 첫번째 인자로 put 요청을 보내고
        // 두번째인자로 url을 보내는데, 뒤에 파라미터로 id값을 붙여보냅니다. 위에서 받아온 id(uuid의 v4()함수로생성된..)를 붙여서
        // 세번째 인자론 textarea에 입력한 text값과, 현재 url의 localhost:3000/?userId=roy 여기의 userId를 보냅니다.
    const newMsg = await fetcher('put', `/messages/${id}`, {text, userId});
    // ...
}


// ...
const setMsgs = data => writeDB('messages', data)
// ...
// 위의 MsgList 컴포넌트에서 PUT(Update) 요청을 보내면
// 아래의 server/src/routes/messages.js에 정의되어있는 REST API의 PUT(UPDATE) 라우터가 실행됩니다.
const messagesRoute = [
    // ...
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id', // 위에서 fetcher 함수를 통해 두번째 인자로 '/messages/${id}'를 전달했었습니다.
                                // 이것을 아래 {params: {id}}라는 객체분할할당을 통해 id만 빼낼 수 있습니다. 
        handler: ({body, params: {id}}, res) => {
            try {
                const msgs = getMsgs();
                // 위에서 빼낸 id와 현재 메시지 데이터들의 id와 비교해 매칭되는게 있는지 검사합니다.
                // 매친되는 것이 있다면 해당 index 값을 반환할 것이고, 없다면 -1을 반환합니다.
                const targetIndex = msgs.findIndex(msg => msg.id === id) 
                // -1을 반환한다면 메시지가 없다는 에러 메시지를 출력합니다.
                if (targetIndex < 0) throw '메시지가 없습니다.'
                // 매칭되는 id가 있지만 사용자가 다르다면 사용자가 다르다는 메시지를 출력합니다.
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                // 만약 위의 코드에 안 걸렸다면 아래와 같이 newMsg를 작성합니다.
                const newMsg = {
                    ...msgs[targetIndex], 
                    text: body.text,
                }
                // 그리고 newMsg를 아래와 같이 splice 메소드로 중간에 넣어주고
                msgs.splice(targetIndex, 1, newMsg)
                // setMsgs() 함수를 통해 DB에 기록합니다.
                setMsgs(msgs)
                // 그리고 응답으론 새로 업데이트된 메시지만 보냅니다.
                res.send(newMsg) 
            } catch (err) {
                // 에러가난다면 상태값은 500으로하고 에러 메시지를 보냅니다.
                res.status(500).send({error: err})
            }
        }
    },
]


const [editingId, setEditingId] = useState(null);
const [msgs, setMsgs] = useState(smsgs);
const doneEdit = () => setEditingId(null);

const onUpdate = async (text, id) => {
    // 위에서 res.send(newMsg)로 전달온 응답값이 아래 newMsg 변수에 담깁니다.
    const newMsg = await fetcher('put', `/messages/${id}`, {text, userId});
    // 만약 newMsg에 아무것도 담기지 않았다면 에러를 발생시킵니다. 안전장치입니다.
    if (!newMsg) throw Error('something wrong');

    // newMsg에 업데이트된 값이 제대로 담겨져왔다면 아래와 같이 state의 msgs를 업데이트해줍니다.
    setMsgs(msgs => {
        // 기존 msgs에서 id 값과 일치하는 것이 있는지보고,
        const targetIndex = msgs.findIndex(msg => msg.id === id);
        // 일치하는 것이 없다면 msgs를 반환합니다. - 이 과정은 서버쪽 REST API와 동일하죠? 안전장치를 2중으로 두는 개념인거 같습니다.
        if (targetIndex < 0) return msgs;
        // newMsgs 변수에 msgs를 deep copy를 합니다.
        const newMsgs = [...msgs]
        // 그리고 deep copy한 newMsgs에 newMsg를 업데이트해줍니다.
        newMsgs.splice(targetIndex, 1, newMsg);
        // 그리고 해당 값을 반환하면 msgs state 값이 바뀝니다. 
        // state 값이 바뀌었으므로 화면이 다시 렌더링됩니다.
        return newMsgs;
    })
    // 새글 등록 및 업데이트가 끝났음을 의미하는 doneEdit() 함수를 호출합니다.
    // editingId의 값을 null로 바꿔 MsgInput 컴포넌트를 노출시키지 않습니다.
    doneEdit();
}


Note

위와 같이 잘 수정되는 것을 보실 수 있습니다.
현재 localhost:3000/?userId=jay로 접속했기 때문에 jay로 등록된 메시지들만 수정이됩니다.

위와 같이 수정 요청이 잘 가고, 새로고침해도 수정된 내용이 잘 유지되는 것을 볼 수 있습니다.

Note

위와 같이 {id:..., text:..., timestamp:..., userId:...} 형태로 PUT(UPDATE) 요청이 갑니다.

Note

이상한 id 값은 uuid 라이브러리의 v4() 함수를 통해 생성되는 id 값입니다.


import {v4} from "uuid";
const getMsgs = () => readDB('messages');
// ...
const messagesRoute = [
    // ...
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        handler: ({body}, res) => {
            try {
                if (!body.userId) throw Error('no userId');

                const msgs = getMsgs();
                const newMsg = {
                    id: v4(), // 새글 등록을하면 이렇게 id 값이 uuid 라이브러리의 v4() 함수로 생성되기 때문에 위와 같이 이상한 값으로 id 값이 설정되어 요청이 가는 것이다.
                    text: body.text,
                    userId: body.userId,
                    timestamp: Date.now(),
                }
                msgs.unshift(newMsg) 
                setMsgs(msgs);
                res.send(newMsg);
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]

Note

위와 같이 id가 생성된 것을 확인할 수 있다.

아래와 같이 server/src/db/messages.json 파일이 수정된 것을 볼 수 있다.

DB 역할을 수행하고 있다는 것이죠.

구조정리

messages.json 데이터 형태


// 현재 messages.json에 기록되어있은 Database입니다.
// 보시면 배열 형태이고, 각 요소는 {} 객체 형태로 들어가있습니다.
// id, text, userId, timestamp 이렇게 4개의 프로퍼티 키가 있습니다.
// 처음에 이 더미 데이터를 만들었을 땐, id 값을 1~50으로 생성했었습니다.
// timestamp 또한 특정 시간에서 -1분씩해서 50개를 생성했었구요.
[
    {"id":"2e0c7e5e-7745-4b89-ae66-75ac67ec7839","text":"로이라는 닉네임으로 새글이 등록됩니다.","userId":"roy","timestamp":1631433146774},
    {"id":"398a4852-0d87-43e0-ae9d-d681d1dc2297","text":"제이라는 닉네임으로 새글이 등록됩니다.","userId":"jay","timestamp":1631433059221},
    // ...
]

REST API - POST(CREATE) 형태


// server/src/routes/messages.js
// 그런데 실습을하면서 새글을 POST(CREATE)하면 아래 라우터를 통해 등록이 되기 때문에
// id값은 uuid라는 라이브러리의 v4() 함수를 통해 생성되고,
// timestamp 값도 Date.now() 함수를 통해 생성되기에 
// 기존에 수동으로 만든 50개의 데이터와는 차이가 있습니다.
import {v4} from "uuid";
const getMsgs = () => readDB('messages');
const setMsgs = data => writeDB('messages', data);
// ...
const messagesRoute = [
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        handler: ({body}, res) => {
            try {
                if (!body.userId) throw Error('no userId');

                const msgs = getMsgs();
                const newMsg = {
                    id: v4(), 
                    text: body.text,
                    userId: body.userId,
                    timestamp: Date.now(),
                }
                msgs.unshift(newMsg);
                setMsgs(msgs);
                res.send(newMsg);
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]


// MsgList 컴포넌트에서 리스트를 처음 렌더링할 때 useEffect 훅이 실행되면서 getMessages가 실행됩니다.
// 원래 msgs는 빈배열이었지만 getMessages가 실행되면서 messages.json에 담겨있는 데이터를 가져와 msgs에 setMsgs라는 state 변경 함수를 통해 담습니다.
// state가 변경되었기 때문에 렌더링이 다시 발생되어 메시지가 화면에 뿌려집니다.
const [msgs, setMsgs] = useState([]); 
const getMessages = async () => {
    const msgs = await fetcher('get', '/messages');
    setMsgs(msgs);
}
useEffect(() => {
    getMessages();
}, []);


// MsgList의 onCreate 메소드를 보겠습니다.
// onCreate 메소드는 새글 등록이므로 인자로 새글값인 textarea의 value인 text만 전달합니다.
const onCreate = async text => {
    // fetcher 함수를 통해 axios 모듈로 서버에 요청을 보냅니다.
        // 첫번째 인자로 'post'를 보냅니다. POST 요청이란 뜻입니다.
        // 두번째 인자로 url을 보냅니다. '/messages' 
        // 세번째 인자로 새글등록이므로 그 글에 대한 내용인 text와 현재 누가 접속했는지(localhost:3000/?userId=jay) 알려주는 userId를 객체 형태로 넘겨줍니다.
    const newMsg = await fetcher('post', '/messages', {text, userId});
    if (!newMsg) throw Error('something wrong');
    setMsgs(msgs => ([newMsg, ...msgs]));
}


// client/fetcher.js
// 위의 fetcher 함수는 아래와 같이 생겼습니다.
// axios 라이브러리를 통해 서버로 요청을 보냅니다.
// 요청을 보내고 반환되는 값에서 data만 return을 하도록 코드를 작성했습니다.
const fetcher = async (method, url, ...rest) => {
    const res = await axios[method](url, ...rest)
    return res.data
}


// server/src/index.js
// ...
import messagesRoute from "./routes/messages.js";
import usersRoute from "./routes/users.js";

// ...
// 아래 라우터를 통해 POST를 담당하는 라우터로 들어가게됩니다.
const routes = [...messagesRoute, ...usersRoute]
routes.forEach(({method, route, handler}) => {
    app[method](route, handler)
})

// ...


// server/src/routes/messages.js
// 아래 라우터를 통해 새글이 등록됩니다.
// id를 v4() 함수를 통해 생성하고,
// text를 body.text로 새로운 text로 등록하고
// userId에도 localhost:3000/?userId=jay라면 jay값을 담아주고
// timestamp에도 Date.now() 값을 담아주고
// 이렇게 생성한 newMsg를 unshift 메소드를 통해 배열의 맨 앞에 넣어주고
// setMsgs 함수를 통해 DB 업데이트를 해줍니다.
// 그리고 res.send()를 통해 새로 POST될 메시지를 응답값으로 보냅니다.
import {v4} from "uuid";
const getMsgs = () => readDB('messages');
const setMsgs = data => writeDB('messages', data);
// ...
const messagesRoute = [
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        handler: ({body}, res) => {
            try {
                if (!body.userId) throw Error('no userId');

                const msgs = getMsgs();
                const newMsg = {
                    id: v4(), 
                    text: body.text,
                    userId: body.userId,
                    timestamp: Date.now(),
                }
                msgs.unshift(newMsg);
                setMsgs(msgs);
                res.send(newMsg);
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]

next/router 라이브러리의 useRouter

여기서 짚고넘어가야될게 있습니다.


// client/components/MsgList.js
// 모든 client의 CRUD는 아래 userId의 영향을 받습니다.
// 지금 현재 실습하는 것은 localhost:3000/?userId=jay 이런식으로 url에 직접 userId라는 파라미터와 jay라는 파라미터 값을 넘깁니다.
// 실제 서비스는 직접 넘기진않고 로그인 기능을 구현해 로그인을 하면 url에 위와 같이 localhost:3000/?userId=jay 이렇게 되도록 할 것입니다.
// 그런데 여기선 일단 실습이므로 로그인 기능을 만들진 않았습니다.
// 아래 next/router 라이브러리는 localhost:3000/?userId=jay에서 jay를 req.body로 넘겨주기위해 불러온 라이브러리입니다.
import {useRouter} from "next/router";
// ...
const MsgList = ({smsgs, users}) => {
    // userRouter를 호출해 객체분해할당으로 query를 빼내고
    const {query} = useRouter();
    // 그 query에서 userId를 빼냅니다. (jay나 roy같은거)
    // 아래와 같이 작성한 이유는 window OS에서 localhost:3000/?userId=jay에서 userId를 userid라고 인식하는 경우도 있기 때문입니다.
    const userId = query.userId || query.userid || '';
    // ...
}

REST API - POST(UPDATE)


const MsgList = ({smsgs, users}) => {
    const {query} = useRouter();
    const userId = query.userId || query.userid || '';
    // ...
    // POST(UPDATE)를 하기 위해서 바뀐 text 값, 그리고 어떤 메시지가 바뀌었는지 알기위해 id 값을 인자값으로 넘겨줍니다. 
    // 이 id값은 어디서 넘어올까요?
    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong')
        
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }
    // ...
    return (
        <>
            {userId && <MsgInput mutate={onCreate}/>}
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {/* 아래와 같이 x의 값을 풀어서(spread) 넘겨줬는데, 이는 메시지 요소하나하나가 가지고있는 id, text, timestamp, userId 속성을 풀어서 넘겨주는 겁니다. */}
                                           {...x}
                                           {/* 아래에 onUpdate props로 onUpdate 메소드를 넘겨줍니다. */}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} 
                                           startEdit={() => setEditingId(x.id)} 
                                           isEditing={editingId === x.id}
                                           myId={userId}
                                           user={users[x.userId]}
                    />)
                }
            </ul>
            <div ref={fetchMoreEl} />
        </>
    )
}


// client/components/MsgItem.js
// 그리고 MsgList 컴포넌트에서 넘겨준 id를 MsgItem 컴포넌트에서 props로 받습니다.

const MsgItem = ({
    id,
    userId,
    timestamp,
    text,
    onUpdate,
    onDelete,
    isEditing,
    startEdit,
    myId,
    user
                 }) => (
    <li className='messages__item'>
        <h3>
            {user.nickname}{' '}
            <sub>
                {new Date(timestamp).toLocaleTimeString('ko-KR', {
                    year: 'numeric',
                    month: 'numeric',
                    day: 'numeric',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: true // 오전 오후 시간이 보여지도록 하는 설정
                })}
            </sub>
        </h3>
        {isEditing ? (
            <>
                {/* 그리고 아래 MsgInput 컴포넌트의 props로 mutate로 onUpdate를 넘기고, text로 text를 넘기고, id로 id를 넘깁니다. */}
                <MsgInput mutate={onUpdate} text={text} id={id}/>
            </>
        ): text}

        {myId === userId &&
        <div className="messages__buttons">
            <button onClick={startEdit}>수정</button>
            <button onClick={onDelete}>삭제</button>
        </div>
        }
    </li>
)

export default MsgItem


// client/components/MsgInput.js
import {useRef} from "react";

// MsgItem 컴포넌트에서 받은 id값이 아래 MsgInput 컴포넌트에서 객체분해할당을 통해 id props로 들어옵니다.
// MsgInput 컴포넌트는 id값을 안 받을 때도 있으므로 id의 default 값을 정의했습니다.
// mutate props엔 onUpdate 메소드가 들어오고 인자값으로 text와 id를 넘깁니다. 
// 이렇게 id값이 onUpdate 메소드로 전달되는 것입니다.
const MsgInput = ({mutate, text = '', id = undefined}) => {
    const textRef = useRef(null);
    const onSubmit = e => {
        e.preventDefault();
        e.stopPropagation();
        const text = textRef.current.value;
        textRef.current.value = '';
        mutate(text, id);
    }

    return (
        <form className='messages__input' onSubmit={onSubmit}>
            <textarea
                ref={textRef}
                defaultValue={text}
                placeholder="내용을 입력하세요."
            />
            <button type='submit'>완료</button>
        </form>
    )
}

export default MsgInput


const MsgList = ({smsgs, users}) => {
    const {query} = useRouter();
    const userId = query.userId || query.userid || '';
    // ...
    const onUpdate = async (text, id) => {
        // 여튼 위와 같이 받아온 id를 아래 fetcher 함수의 두번째 인자인 url의 맨 끝에 붙여서 보냅니다.
        // 그리고 세번째 인자로 새로 업데이트할 내용인 text와 userId를 보냅니다.
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong')
        
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }
    // ...
}


const getMsgs = () => readDB('messages');
const setMsgs = data => writeDB('messages', data);
// ...
const messagesRoute = [
    // ...
    // 아래 put 라우터로 위에서 보낸 put 요청이 들어오게됩니다.
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id',
        // client에서 /messages/${id}로 넘긴 id 값이 아래 {params: {id}} 이렇게 구조분해 할당으로 id 값에 담깁니다.
        handler: ({body, params: {id}}, res) => {
            try {
                // getMsgs() 함수로 DB에있는 메시지들 정보를 불러옵니다.
                const msgs = getMsgs();
                // 불러온 메시지들의 id와 받아온 id를 비교해서 일치하는게 있는지 검사합니다.
                const targetIndex = msgs.findIndex(msg => msg.id === id);
                // 일치하는게 없다면 -1 값을 반환하므로 아레 메시지가 없습니다 라는 메시지를 던질것입니다.
                if (targetIndex < 0) throw '메시지가 없습니다.';
                // 일치하는게 있는데, 그 일치하는 메시지의 userId와 body에 넘어온 userId와 일치하지 않는다면, 사용자가 다른 것이기 때문에 사용자가 다릅니다 라는 메시지를 던집니다.
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.';

                // 만약 위에서 걸리지 않았다면 아래에서 새로운 메시지 데이터를 만듭니다.
                const newMsg = {
                    ...msgs[targetIndex],
                    text: body.text,
                }
                // 그리고 새로운 메시지 데이터를 기반으로 업데이트를 해주고
                msgs.splice(targetIndex, 1, newMsg);
                // DB 업데이트를 해줍니다.
                setMsgs(msgs);
                // 그리고 새로운 메시지 데이터를 응답값으로 보냅니다.
                res.send(newMsg); 
            } catch (err) {
                res.status(500).send({error: err});
            }
        }
    },
]


const MsgList = ({smsgs, users}) => {
    const {query} = useRouter();
    const userId = query.userId || query.userid || '';
    const [msgs, setMsgs] = useState(smsgs);
    const [editingId, setEditingId] = useState(null);
    // ...
    const doneEdit = () => setEditingId(null);
    const onUpdate = async (text, id) => {
        // 위에서 응답받은 새로운 메시지 데이터를 newMsg 변수에 담습니다.
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        // newMsg에 아무것도 담기지 않았다면 에러를 발생시킵니다.
        if (!newMsg) throw Error('something wrong')
        
        // newMsg에 어떠한 값이 담겼다면 setMsgs라는 state 값을 수정하는 함수를 통해
        setMsgs(msgs => {
            // 이렇게 매칭되는 값이 있는지 한번 더 찾고
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // 매칭되는 값이 없으면 그냥 원래 msgs 값을 리턴하고
            if (targetIndex < 0) return msgs;
            // 있다면 아래와 같이 deep copy를 한 다음에
            const newMsgs = [...msgs]
            // 새로 업데이트를 하고
            newMsgs.splice(targetIndex, 1, newMsg);
            // 업데이트된 메시지들을 반환하여 state 값을 수정합니다.
            return newMsgs;
        })
        // 그리고 업데이트가 완료되었다는 doneEdit() 함수를 호출합니다.
        doneEdit();
    }
    // ...
    const getMessages = async () => {
        const newMsgs = await fetcher('get', '/messages', {params: {cursor: msgs[msgs.length - 1]?.id || ''}})
        if (newMsgs.length === 0) {
            setHasNext(false);
            return
        }
        setMsgs([...msgs, ...newMsgs])
    }

    useEffect(() => {
        if (intersecting && hasNext) getMessages()
    }, [intersecting])
}

REST API - POST(DELETE)


const MsgList = ({smsgs, users}) => {
    // ...
    // delete는 id값만 알면되기에 id만 넘겨줍니다.
    const onDelete = async (id) => {
        // 넘겨받은 id를 아래와 같이 /messages/${id} 형태로 넘겨줍니다.
        // userId는 params로 넘겨줍니다.
        // 아래는 const receivedId = await fetcher('delete', `/messages/${id}?userId=${userId}`); 라고 작성해주셔도됩니다.
        // 두번째 인자에 다 붙여서 넘겨주셔도되고 아래와 같이 params라는 프로퍼티에 userId 라는 값을 넣어서 보내셔도됩니다.
        const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})

        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1)
            return newMsgs;
        })
    }
    // ...
}


const messagesRoute = [
    // ...
    { // DELETE MESSAGE
        method: 'delete',
        route: '/messages/:id',
        // 특이한게 params로 넘긴 userId 값을 이렇게 서버의 라우터에선 query: {userId}로 뽑아내야합니다.
        // params -> query
        // 매칭이안되어 좀 아쉽습니다.
        handler: ({params: {id}, query: {userId}}, res) => {
            try {
                // 아래와 같이 getMsgs()를 통해 메시지들을 전부 가져오고
                const msgs = getMsgs();
                // 메시지들의 id와 넘겨받은 id와 일치하는게 있는지 확인합니다.
                const targetIndex = msgs.findIndex(msg => msg.id === id)
                // 일치하는게 없다면 메시지가 없습니다. 메시지를 던지고
                if (targetIndex < 0) throw '메시지가 없습니다.'
                // 일치하는게 있는데 userId가 서로 다르다면 사용자가 다릅니다 라는 메시지를 던집니다.
                if (msgs[targetIndex].userId !== userId) throw '사용자가 다릅니다.'

                // 메시지를 삭제하고
                msgs.splice(targetIndex, 1)
                // 해당 id값을 응답값으로 보냅니다.
                res.send(id)
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
    // ...
]


const MsgList = ({smsgs, users}) => {
    // ...
    const onDelete = async (id) => {
        // 응답값으로 받은 id를 receivedId에 담습니다.
        const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})

        setMsgs(msgs => {
            // 마찬가지로 각 메시지들의 id와 대조를하여 일치하는 것이 있는지보고
            // receivedId 뒤에 '' 빈 문자열을 더한 이유는 receivedId가 문자열이어야 하기 때문입니다.
            // receivedId가 문자열이 아닐수도 있는 이유는 처음에 수동으로 생성한 메시지들은 1~50까지의 숫자이기 때문입니다.
            // 물론 id 프로퍼티키에 값을 정의할 땐 '1' 이런식으로 문자열로 지정했지만, 숫자열로 인식될수 있는 경우엔 문자열로 넣어도 숫자열로 변형됩니다.
            // 따라서 다시 뒤에 '' 빈 문자열을 더해줌으로써 문자열 데이터로 바꾸는 겁니다.
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
            // 매칭되는게 없다면 msgs를 반환합니다.
            if (targetIndex < 0) return msgs;
            // 매칭되는게 있다면 deep copy를 하고
            const newMsgs = [...msgs]
            // 해당 메시지를 삭제한 후
            newMsgs.splice(targetIndex, 1)
            // 업데이트된 메시지들을 반환합니다.
            return newMsgs;
        })
    }
    // ...
}

클라이언트에서 DELETE 기능 구현 (Delete)

공부하는 중.. 아직 정리 덜끝남..


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";

const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.

    // 지금 userId를 받아온 현상태에서 MsgItem으로 넘겨주시면 MsgItem에서도 수정/삭제버튼 노출 여부를 결정할 수 있겠죠?

    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        if (!newMsg) throw Error('something wrong') // 여기도 안전 장치를 둡시다.
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    // 이번엔 update 기능(PUT)을 수정하겠습니다.
    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong') // 안전장치. newMsg가 없다면 아무것도 하지 않도록 설정

        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            // 아래 3번째 인자만 newMsg로 수정하면 됩니다.
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    // delete도 구현해보도록 하겠습니다. 마찬가지로 async로 하겠습니다.
    const onDelete = async (id) => {
        // 서버에 보시면 delete에 대한 응답을 삭제된 id 값을 넘겨주도록 되어있었기 때문에
        // 변수명을 receivedId라고 하겠습니다.
        const receivedId = await fetcher('delete', `/messages/${id}`, {userId})

        setMsgs(msgs => {
            // 그리고 아래서 msg.id와 receivedId와 비교를 하면 되겠죠.
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                                           myId={userId}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;


// server/src/routes/messages.js
import {v4} from "uuid";
import {readDB, writeDB} from "../dbController.js";

const getMsgs = () => readDB('messages') // 중복을 방지하기위한 코드입니다.
const setMsgs = data => writeDB('messages', data) // 중복을 방지하기위한 코드입니다.
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: (req, res) => {
            // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다. 다 똑같으니까 이거를 함수로 바꿔봅시다.
            const msgs = getMsgs();
            res.send(msgs)
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        method: 'get',
        route: '/messages/:id',
        handler: ({params: {id}, res}) => { // id를 직접 받아오기 때문에 에러가날 가능성이 있으므로 여기도 마찬가지로 에러 처리를 해줍니다.
            try {
                const msgs = getMsgs();
                const msg = msgs.find(m => m.id === id) // 이번엔 findIndex 메소드가 아니라 find 메소드를 사용합니다.
                if (!msg) throw Error('not found')
                res.send(msg) // msg를 send해줍니다.
            } catch (err) {
                res.status(404).send({ error: err })
            }
        }
    },
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        // POST는 새글을 등록하는겁니다.
        // 첫번째(request) 인자에는 body, params, query가 있습니다. 그 중에서 body를 사용하게됩니다.
        // body는 새글이 등록된 text가 들어있을거고 그리고 userId도 들어있습니다.
        handler: ({body}, res) => {
            // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
            const msgs = getMsgs();
            const newMsg = {
                id: v4(), // uuid의 v4 버전의 id를 만들겠다는 뜻입니다.
                text: body.text,
                userId: body.userId,
                timestamp: Date.now(),
            }
            msgs.unshift(newMsg) // 새글을 배열의 제일 앞에 넣어줍니다.
            // writeDB('messages', msgs) // 그리고 DB에 기록합니다. // 이 코드도 자주 사용하므로 위에 함수로 뺍니다.
            setMsgs(msgs);
            res.send(newMsg) // 그리고 응답은 업데이트된 메시지만 보내면 될겁니다.
        }
    },
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id', // <- UPDATE는 이렇게 실제 id를 지정해서 요청을 보내는겁니다.
        // UPDATE는 body에 변경된 text가 들어올거고 params 안에 id가 들어오게됩니다.
        // 이런 부분은 여러분들이 첫번째 인자(request)를 콘솔에 출력해보시면 확인하실 수 있으실겁니다.
        handler: ({body, params: {id}}, res) => {
            // UPDATE 요청은 위의 :id로 실제 id로 요청을 보내는거다보니까 클라이언트에선 id가 나와있는데,
            // 실제 서버에선 없는 경우, 혹은 그 반대인 경우,
            // 이런식으로 서버와 클라이언트간 싱크가 맞지 않아서 오류가날 가능성이 없진 않을겁니다.
            // 그래서 그런 경우에 대한 안전대비책을 해놓고 가겠습니다.
            try {
                // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id) // targetIndex 찾는 방법은 똑같습니다.
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                const newMsg = {
                    ...msgs[targetIndex], // 기존내용을 다 담고
                    text: body.text, // text만 새로 담으면됩니다.
                }
                msgs.splice(targetIndex, 1, newMsg)
                setMsgs(msgs)
                res.send(newMsg) // 새로 변경된 메시지를 send
            } catch (err) {
                res.status(500).send({error: err}) // error가 날 경우 status를 500으로 지정하고 err 메시지를 띄웁니다.
            }
        }
    },
    { // DELETE MESSAGE
        // 서버에서 보시면 delete에 대한 응답이 아래처럼 id만 넘겨주도록 되어있었죠?
        method: 'delete',
        route: '/messages/:id',
        handler: ({body, params: {id}}, res) => {
            try {
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id)
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                msgs.splice(targetIndex, 1)
                res.send(id) // DELETE 성공했을 때, id만 넘겨주면될겁니다. 이 id가 지워졌어요 라는 메시지를 던져주는겁니다.
            } catch (err) {
                res.status(500).send({error: err}) // 실패하였을 땐 에러메시지를 던져줍니다.
            }
        }
    },
]

export default messagesRoute

Note

이렇게 사용자가 다르다는 500 에러가 떴습니다.
흐음.. 사용자가 다르다고?
왜 다른걸까?

Note

위에 보시면 messages.jsdelete 라우터에서 body.userId가 안들어와서 생긴 에러입니다.
MsgList 컴포넌트에서 {userId} 부분으로 userId를 넘기는거 같지만, 그래서 이 부분이 body에 담겨서 보내져야될거같지만,

axios에서 get 메소드와 delete 메소드는 애초에 2번째 인자값이 post, put처럼 data가 아니라 옵션(config)값입니다.
그렇기 때문에 delete 메소드에서 두번째 인자({userId})로 넘겨주는 userId라는 객체는 config 객체 안으로 들어가는겁니다.

Note

그래서 위와같이 {params: {userId}} 이렇게 넘겨야합니다.
위와 같이 넘기는 것이 localhost:3000/messages/${id}?userId=${userId} 이렇게 넘기는 것과 같습니다.
즉, 위와 같이 {params: {userId}}로 넘겨주거나 /messages/${id}?userid?=${userId} 이렇게 넘겨주면됩니다.

하지만 클라이언트에서 params: {userId}로 보내도 서버 라우터에선 query: {userId} 이렇게 받아야합니다.
/message/${id}라고 넘겨준 id는 서버 라우터에서 params: {id}로 받아주는데말이죠..
명칭이 클라이언트와 서버가 서로 매칭이 안됩니다.
이게 좀 아쉬운점입니다.

Note

위와 같이 서버 라우터에서 console.log(req)를 통해 리퀘스트 객체를 콘솔에 찍어봅니다.

onDelete 메소드로 delete 요청을 보낼 때, 세번째 인자로 { params: { userId }}를 보냈는데,
이는 urlqueryString으로 날라가게되고,
이를 서버의 router 핸들러에서 query: {userId}로 객체분해할당 문법을 통해서 받는다.

즉, params로 보냈는데 서버에선 query로 받는다.

Note

50번 삭제 버튼을 눌렀더니 응답값으로 50번이 왔습니다.
그 말은 성공했다는 의미입니다.

그런데.. 성공했는데 왜 안지워질까요?

Note

위는 콘솔창에 찍힌 req입니다.
엄청 깁니다.

오잉 1~50번까지 수동으로 생성한 데이터는 삭제버튼 눌르면 응답값은 제대로 오는데, 화면에서 안 사라지는데,
새글등록으로 등록해서 만들어진 것들은 삭제 버튼 누르면 잘 삭제된다.
이게 어떻게된거지??

Note

위와 같이 reqparamsquery가 있는 것을 확인하실 수 있습니다.
이 부분에서 userId를 가져오는겁니다.

저로써는 이런 부분이 이해하기가 힘들었습니다. (params: {id: '50'}, query: {userId: 'jay'})
이것이 잘 안 와닿았던 이유가

Note

클라이언트쪽 onDelete 메소드에서 분명 {query: {userId}} 이렇게 넘겨줬었는데,
서버의 req에는 query로 들어간다는거..
이런거에 대한 명칭 통일이 되면 좋겠다 싶은 생각이 있습니다.

여튼 이렇게 수정하면 동작은 정상적으로 작동합니다.


오잉 1~50번까지 수동으로 생성한 데이터는 삭제버튼 눌르면 응답값은 제대로 오는데, 화면에서 안 사라지는데,
새글등록으로 등록해서 만들어진 것들은 삭제 버튼 누르면 잘 삭제된다.
이게 어떻게된거지??

아까 위에서 이렇게 짚었었다.

성공하긴 성공했는데 화면상에서 지워지지 않았다.
그 이유는 뷰단에서 처리를 해주지 못하고 있기 때문입니다.
왜냐면

Note

위와 같이 새로고침을하면 50번이 삭제된 것이 화면에 반영된것을 볼 수 있습니다.
이 문제를 해결해보도록 하겠습니다.

Note

위와 같이 receivedIdid의 타입을 찍어보겠습니다.
msgs.findIndex(msg =&gt; msg.id === receivedId) 두개의 타입이 달라서 여기서 완전히 일치하지 않기 때문에 targetIndex를 찾질못해 view단에서 아무일도 일어나지 않았던 것입니다.

Note

create로 새로 만들어진 것은 receivedIdid나 모두 String으로 나옵니다.
그런데 기존 더미 데이터 수동으로 만든 것들은 number, string으로 나옵니다.

receivedId 값이 number 형태로 들어오고 있습니다.
원래는 string 타입으로 지정을 했는데,

Note

새로 추가된 것들의 id는 확실하게 string인데,
예전 임의로 만든 것들은 number로 바뀔 수 있기 때문입니다.

무슨 말이냐면 서버에서는 json 형태로 데이터가 내려올텐데, 그 json을 다시 parse를 하는 과정에서 해당 값이 number로 간주될 수 있으면 그냥 number로 바꿔버리는 겁니다.
형변환이 자동으로 이루어지기 때문에 생기는 문제입니다.
이 문제를 해결하기 위해서는 receivedIdstring으로 전환해줄 필요가 있겠죠.
다양한 방법이 있는데, 아래와 같이 간단하게 '' 문자열을 더해주는 것으로 해보겠습니다.

그럼 옛날 1~50번 사이의 데이터를 삭제를 할 때, 바로 뷰단에서 반영이 되는 것을 확인하실 수 있을겁니다.

새로 post(create)되는 데이터들은 문제가 없습니다.
새로 생성되는 데이터들의 id 값은 확실한 string으로 만들어지는데, 저희가 최초에 목데이터로 생성한 1~50까지의 데이터는
string이면서 number로 형변환이 가능한 애들이기 때문에 발생한 문제입니다.


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";

// 이제 필요없어진 아래 코드들은 지우거나 주석처리합시다.
// const UserIds = ['roy', 'jay'];
// const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.

    // 지금 userId를 받아온 현상태에서 MsgItem으로 넘겨주시면 MsgItem에서도 수정/삭제버튼 노출 여부를 결정할 수 있겠죠?

    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        if (!newMsg) throw Error('something wrong') // 여기도 안전 장치를 둡시다.
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    // 이번엔 update 기능(PUT)을 수정하겠습니다.
    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong') // 안전장치. newMsg가 없다면 아무것도 하지 않도록 설정

        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            // 아래 3번째 인자만 newMsg로 수정하면 됩니다.
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    // delete도 구현해보도록 하겠습니다. 마찬가지로 async로 하겠습니다.
    const onDelete = async (id) => {
        // 서버에 보시면 delete에 대한 응답을 삭제된 id 값을 넘겨주도록 되어있었기 때문에
        // 변수명을 receivedId라고 하겠습니다.
        // 그래서 아래와 같이 params: { userId } 이렇게 넘겨야합니다.
        // 이렇게 넘기는 것이 localhost:3000/messages/${id}?userId=${userId} <- 이렇게 넘기는 것과 같습니다.
        // 즉, 아래와 같이 params: {userId}로 보내주거나
        // const receivedId = await fetcher('delete', `/messages/${id}?userId=${userId}`) <- 이렇게 작성하면됩니다.
        // 일단 저희는 params로 보내보겠습니다.
        // 이렇게 params로 보내줬지만 실제로는 queryString이 되어서 보내지게 되는 거죠.
        const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})

        setMsgs(msgs => {
            // 그리고 아래서 msg.id와 receivedId와 비교를 하면 되겠죠.
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            <MsgInput mutate={onCreate}/>
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                                           myId={userId}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;


// server/src/routes/messages.js
import {v4} from "uuid";
import {readDB, writeDB} from "../dbController.js";

const getMsgs = () => readDB('messages') // 중복을 방지하기위한 코드입니다.
const setMsgs = data => writeDB('messages', data) // 중복을 방지하기위한 코드입니다.
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: (req, res) => {
            // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다. 다 똑같으니까 이거를 함수로 바꿔봅시다.
            const msgs = getMsgs();
            res.send(msgs)
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        method: 'get',
        route: '/messages/:id',
        handler: ({params: {id}, res}) => { // id를 직접 받아오기 때문에 에러가날 가능성이 있으므로 여기도 마찬가지로 에러 처리를 해줍니다.
            try {
                const msgs = getMsgs();
                const msg = msgs.find(m => m.id === id) // 이번엔 findIndex 메소드가 아니라 find 메소드를 사용합니다.
                if (!msg) throw Error('not found')
                res.send(msg) // msg를 send해줍니다.
            } catch (err) {
                res.status(404).send({ error: err })
            }
        }
    },
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        // POST는 새글을 등록하는겁니다.
        // 첫번째(request) 인자에는 body, params, query가 있습니다. 그 중에서 body를 사용하게됩니다.
        // body는 새글이 등록된 text가 들어있을거고 그리고 userId도 들어있습니다.
        handler: ({body}, res) => {
            // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
            const msgs = getMsgs();
            const newMsg = {
                id: v4(), // uuid의 v4 버전의 id를 만들겠다는 뜻입니다.
                text: body.text,
                userId: body.userId,
                timestamp: Date.now(),
            }
            msgs.unshift(newMsg) // 새글을 배열의 제일 앞에 넣어줍니다.
            // writeDB('messages', msgs) // 그리고 DB에 기록합니다. // 이 코드도 자주 사용하므로 위에 함수로 뺍니다.
            setMsgs(msgs);
            res.send(newMsg) // 그리고 응답은 업데이트된 메시지만 보내면 될겁니다.
        }
    },
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id', // <- UPDATE는 이렇게 실제 id를 지정해서 요청을 보내는겁니다.
        // UPDATE는 body에 변경된 text가 들어올거고 params 안에 id가 들어오게됩니다.
        // 이런 부분은 여러분들이 첫번째 인자(request)를 콘솔에 출력해보시면 확인하실 수 있으실겁니다.
        handler: ({body, params: {id}}, res) => {
            // UPDATE 요청은 위의 :id로 실제 id로 요청을 보내는거다보니까 클라이언트에선 id가 나와있는데,
            // 실제 서버에선 없는 경우, 혹은 그 반대인 경우,
            // 이런식으로 서버와 클라이언트간 싱크가 맞지 않아서 오류가날 가능성이 없진 않을겁니다.
            // 그래서 그런 경우에 대한 안전대비책을 해놓고 가겠습니다.
            try {
                // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id) // targetIndex 찾는 방법은 똑같습니다.
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                const newMsg = {
                    ...msgs[targetIndex], // 기존내용을 다 담고
                    text: body.text, // text만 새로 담으면됩니다.
                }
                msgs.splice(targetIndex, 1, newMsg)
                setMsgs(msgs)
                res.send(newMsg) // 새로 변경된 메시지를 send
            } catch (err) {
                res.status(500).send({error: err}) // error가 날 경우 status를 500으로 지정하고 err 메시지를 띄웁니다.
            }
        }
    },
    { // DELETE MESSAGE
        // 서버에서 보시면 delete에 대한 응답이 아래처럼 id만 넘겨주도록 되어있었죠?
        method: 'delete',
        route: '/messages/:id',
        // queryString으로 오기 때문에 아래선 query: {userId}로 받아야됩니다.
        handler: ({params: {id}, query: {userId}}, res) => {
            try {
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id)
                if (targetIndex < 0) throw '메시지가 없습니다.'
                // 아래도 수정해줍시다.
                if (msgs[targetIndex].userId !== userId) throw '사용자가 다릅니다.'

                msgs.splice(targetIndex, 1)
                res.send(id) // DELETE 성공했을 때, id만 넘겨주면될겁니다. 이 id가 지워졌어요 라는 메시지를 던져주는겁니다.
            } catch (err) {
                res.status(500).send({error: err}) // 실패하였을 땐 에러메시지를 던져줍니다.
            }
        }
    },
]

export default messagesRoute

3.2 (추가) windows - case issue 대응 / userId 예외처리

Note

localhost:3000/?userId=jay에서 userIdI를 windows OS에서 대문자를 소문자로 바꿔버리는 이슈가 있는 거 같습니다.
userId면, userid로 바꿔서 인식한다는겁니다.
그래서 이 부분에 대한 대응 방법을 알려드리겠습니다.

userId라고 입력해도 자동으로 userid로 변경이되는 경우를 대비해서 내용을 수정해보겠습니다.

Note

const {query: {userId = ''}} = useRouter();
위에 보시면 userId를 현재는 I <- 대문자로 받아오게 되어있는데, 이를 소문자이든 대문자이든 상관없게끔 바꿔보도록 하겠습니다.

const {query} = useRouter()로 우선 query만 빼내옵니다.
그리고 const userId = query.userId || query.userid || ''; 이렇게 작성해주시면 queryString으로 소문자로 넘어와도 문제가 없이 잘 작동할겁니다.
그리고 안전장치로 userId에 아무것도 없을 때 '' 빈문자열을 query에 담습니다.

그리고 서버쪽 라우터쪽에서 userId가 빈문자열일 경우, 어떻게 작동해야될지를 정의해보겠습니다.
userId가 빈문자열일 경우, CRUD 모두 아무것도 하지 않게하는 것이 좋을거 같습니다.

Note

GET은 어차피 userId 안보내니깐 상관없고 POST에서 아래와 같이 작성해줍니다.
if (!body.userId) throw Error('no userId');
UPDATEDELETE도 이미 userId 관련 처리가 되어있으니 POST만 처리해주면됩니다.
body.userId가 없으면 에러를 발생시킵니다.

Note

위와 같이 localhost:3000 이렇게 접속한 상태에선 userId가 없으므로 이렇게 userId가 없는 상태에서 요청을 보내면 에러가뜹니다.
이렇게 userId가 없는 상태에서 post가 안되도록 안전장치가 마련이 되었습니다.

한가지 더 해보자면 MsgList.js에서 userId가 없을 경우 아예 input창을 띄우지 않는 것이 좋을거 같아요.

Note

그리고 위와 같이 userId가 있을 때만 MsgInput 컴포넌트를 띄웁니다.

Note

확인해보시면 위와 같이 localhost:3000 이렇게 로그인하지 않은 상태에서는 input창이 안뜹니다.

Note

다시 localhost:3000/?userId=jay 이렇게 로그인 상태로 접속하면 input창이 보입니다.

이 정도면 어느정도의 안전장치는 마련이 된 거 같아서 이런식으로 대비를 해주시면 되겠습니다.


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";

// 이제 필요없어진 아래 코드들은 지우거나 주석처리합시다.
// const UserIds = ['roy', 'jay'];
// const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    // 아래보시면 userId를 현재는 I <- 대문자로 받아오게 되어있는데, 소문자이든, 대문자이든 상관없게끔 바꿔보도록 하겠습니다.
    const {query} = useRouter();
    // 아래와 같이 userId 또는 userid 이렇게 바꿔주시면 queryString으로 소문자로 넘어와도 문제가 없이 잘 작동합니다.
    // 그리고 여기서 안전장치 하나를 걸어두는게 좋을 거 같습니다.
    // userId가 '' 빈문자열일 경우, 어떻게 작동해야될지에 대해서도 작성하는 것이 좋을 거 같습니다.
    // userId가 '' 빈문자열일 경우, CRUD 모두 아무것도 하지 않게하는 것이 좋을거 같습니다.
    // 이 부분에 대해서는 서버에서 처리를 해보겠습니다.
    const userId = query.userId || query.userid || '';
    // const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.

    // 지금 userId를 받아온 현상태에서 MsgItem으로 넘겨주시면 MsgItem에서도 수정/삭제버튼 노출 여부를 결정할 수 있겠죠?

    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        if (!newMsg) throw Error('something wrong') // 여기도 안전 장치를 둡시다.
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    // 이번엔 update 기능(PUT)을 수정하겠습니다.
    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong') // 안전장치. newMsg가 없다면 아무것도 하지 않도록 설정

        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            // 아래 3번째 인자만 newMsg로 수정하면 됩니다.
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    // delete도 구현해보도록 하겠습니다. 마찬가지로 async로 하겠습니다.
    const onDelete = async (id) => {
        // 서버에 보시면 delete에 대한 응답을 삭제된 id 값을 넘겨주도록 되어있었기 때문에
        // 변수명을 receivedId라고 하겠습니다.
        // 그래서 아래와 같이 params: { userId } 이렇게 넘겨야합니다.
        // 이렇게 넘기는 것이 localhost:3000/messages/${id}?userId=${userId} <- 이렇게 넘기는 것과 같습니다.
        // 즉, 아래와 같이 params: {userId}로 보내주거나
        // const receivedId = await fetcher('delete', `/messages/${id}?userId=${userId}`) <- 이렇게 작성하면됩니다.
        // 일단 저희는 params로 보내보겠습니다.
        // 이렇게 params로 보내줬지만 실제로는 queryString이 되어서 보내지게 되는 거죠.
        const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})

        setMsgs(msgs => {
            // 그리고 아래서 msg.id와 receivedId와 비교를 하면 되겠죠.
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const msgs = await fetcher('get', '/messages')
        setMsgs(msgs)
    }
    useEffect(() => {
        getMessages();
    }, [])

    return (
        <>
            {userId && <MsgInput mutate={onCreate}/>}
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                                           myId={userId}
                    />)
                }
            </ul>
        </>
    )
}

export default MsgList;


// server/src/routes/messages.js
import {v4} from "uuid";
import {readDB, writeDB} from "../dbController.js";

const getMsgs = () => readDB('messages') // 중복을 방지하기위한 코드입니다.
const setMsgs = data => writeDB('messages', data) // 중복을 방지하기위한 코드입니다.
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: (req, res) => {
            // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다. 다 똑같으니까 이거를 함수로 바꿔봅시다.
            const msgs = getMsgs();
            res.send(msgs)
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        method: 'get',
        route: '/messages/:id',
        handler: ({params: {id}, res}) => { // id를 직접 받아오기 때문에 에러가날 가능성이 있으므로 여기도 마찬가지로 에러 처리를 해줍니다.
            try {
                const msgs = getMsgs();
                const msg = msgs.find(m => m.id === id) // 이번엔 findIndex 메소드가 아니라 find 메소드를 사용합니다.
                if (!msg) throw Error('not found')
                res.send(msg) // msg를 send해줍니다.
            } catch (err) {
                res.status(404).send({ error: err })
            }
        }
    },
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        // POST는 새글을 등록하는겁니다.
        // 첫번째(request) 인자에는 body, params, query가 있습니다. 그 중에서 body를 사용하게됩니다.
        // body는 새글이 등록된 text가 들어있을거고 그리고 userId도 들어있습니다.
        handler: ({body}, res) => {
            try {
                // GET은 어차피 userId 안보내니깐 상관없고 POST에서 아래와 같이 작성해줍니다.
                // UPDATE나 DELETE도 이미 userId 관련 처리가 되어있으니 POST만 처리해주면됩니다.
                // body.userId가 없으면 에러를 발생시킵니다.
                if (!body.userId) throw Error('no userId');

                // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
                const msgs = getMsgs();
                const newMsg = {
                    id: v4(), // uuid의 v4 버전의 id를 만들겠다는 뜻입니다.
                    text: body.text,
                    userId: body.userId,
                    timestamp: Date.now(),
                }
                msgs.unshift(newMsg) // 새글을 배열의 제일 앞에 넣어줍니다.
                // writeDB('messages', msgs) // 그리고 DB에 기록합니다. // 이 코드도 자주 사용하므로 위에 함수로 뺍니다.
                setMsgs(msgs);
                res.send(newMsg) // 그리고 응답은 업데이트된 메시지만 보내면 될겁니다.
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id', // <- UPDATE는 이렇게 실제 id를 지정해서 요청을 보내는겁니다.
        // UPDATE는 body에 변경된 text가 들어올거고 params 안에 id가 들어오게됩니다.
        // 이런 부분은 여러분들이 첫번째 인자(request)를 콘솔에 출력해보시면 확인하실 수 있으실겁니다.
        handler: ({body, params: {id}}, res) => {
            // UPDATE 요청은 위의 :id로 실제 id로 요청을 보내는거다보니까 클라이언트에선 id가 나와있는데,
            // 실제 서버에선 없는 경우, 혹은 그 반대인 경우,
            // 이런식으로 서버와 클라이언트간 싱크가 맞지 않아서 오류가날 가능성이 없진 않을겁니다.
            // 그래서 그런 경우에 대한 안전대비책을 해놓고 가겠습니다.
            try {
                // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id) // targetIndex 찾는 방법은 똑같습니다.
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                const newMsg = {
                    ...msgs[targetIndex], // 기존내용을 다 담고
                    text: body.text, // text만 새로 담으면됩니다.
                }
                msgs.splice(targetIndex, 1, newMsg)
                setMsgs(msgs)
                res.send(newMsg) // 새로 변경된 메시지를 send
            } catch (err) {
                res.status(500).send({error: err}) // error가 날 경우 status를 500으로 지정하고 err 메시지를 띄웁니다.
            }
        }
    },
    { // DELETE MESSAGE
        // 서버에서 보시면 delete에 대한 응답이 아래처럼 id만 넘겨주도록 되어있었죠?
        method: 'delete',
        route: '/messages/:id',
        // queryString으로 오기 때문에 아래선 query: {userId}로 받아야됩니다.
        handler: ({params: {id}, query: {userId}}, res) => {
            try {
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id)
                if (targetIndex < 0) throw '메시지가 없습니다.'
                // 아래도 수정해줍시다.
                if (msgs[targetIndex].userId !== userId) throw '사용자가 다릅니다.'

                msgs.splice(targetIndex, 1)
                res.send(id) // DELETE 성공했을 때, id만 넘겨주면될겁니다. 이 id가 지워졌어요 라는 메시지를 던져주는겁니다.
            } catch (err) {
                res.status(500).send({error: err}) // 실패하였을 땐 에러메시지를 던져줍니다.
            }
        }
    },
]

export default messagesRoute

3.3 무한스크롤 구현

그런데 메시지를 처음부터 50개를 다 들고오는 것이 좀 부담이 됩니다.
이를 무한스크롤 기능을 적용해서 한번에 몇개씩만 들고오도록 적용을 해보도록 하겠습니다.
이는 프론트엔드의 번외적인 내용인데, 이 내용을 해놓고나면 나중에 배울 GraphQL과 비교해보면서 할 수 있어서 지금 해보도록 하겠습니다.

Note

위에서 무한스크롤링을 위해 useInfiniteScroll()함수를 작성했습니다.
그런데 이 useInfiniteScroll 함수는 MsgList 컴포넌트에서 한번만 호출되는 함수가 아니라
메시지가 새로 등록될 때마다 계속 연거푸 실행되는 함수이기 때문에
여기서도 안전장치가 필요합니다.


우선 useInfiniteScroll 함수에 대해 보겠습니다.

  • intersecting 변수는 현재 화면상에 useReffetchMoreEl이라는걸 참조하고있는데 이 fetchMoreEl이라는 값이 ref 속성으로 맨 아래 div 요소 속성으로 정의되어있다.
    그 요소가 화면상에서 보미면 intersecting 변수가 true가되고 안 보이면 false가 되도록 하는 구조이다.

  • observer: 여기엔 IntersectionObserver라는 객체의 인스턴스를 담습니다.
    IntersectionObserver 인스턴스를 생성할 때 인자값으로 콜백함수를 넘깁니다.

    • entries: 콜백함수의 인자값은 배열 형태로 넘어옵니다.
      보시면 fetchMoreEl이라는 useRef 값이 들어오는데 이게 배열 형태로 넘어가는 것 같습니다. (div라는 요소를 참조해서그런가?)
      여튼 넘겨받은 배열형태의 데이터를 entries.some(entry =&gt; entry.isIntersecting)을 통해 하나라도 truetrue를 반환합니다.
      isIntersecting은 현재 entry가 화면에 보이는지 안보이는지를 말해주는 속성값인거 같습니다.
      즉, 하나라도 화면에 보이면 true를 반환합니다.
  • useEffect: useEffect 훅을 사용합니다.
    [target.current] 값을 감시하면서 이 값이 변경될 때마다 useEffect 내부 코드가 실행되도록 합니다.

    • if (targetEl.current) observer.observe(targetEl.current): targetEl이 있다면 해당 targetEl을 계속 감시하도록합니다.
    • return () =&gt; { observer.disconnect() }: 라이프사이클에서 만약 이 targetEl로 들어온 컴포넌트가 사라지면, diconnect를 해줍니다.
  • return intersecting: intersecting 값을 return함으로써 현재 fetchMoreEl이 보이는지 아닌지를 알려줍니다.

Note

useInfiniteScroll 함수가 MsgList에서 한번만 호출되는 것이 아니기 때문에 아래와 같은 안전장치가 필요합니다.
아래와 같이 수정하기 전에는 useInfiniteScroll 함수가 실행될 때마다 new IntersectionObserver()을 통해 인스턴스가 생성되었습니다.
이를 방지하기 위함입니다.


  • const observerRef = useRef(null);: useRef를 통해 observerRef 변수를 참조합니다.
  • const getObserver = useCallback(() =&gt; {...}): 그리고 getObserver 함수를 만듭니다.
    useInfiniteScroll 함수가 MsgList에서 한번만 호출되는 것이 아니라 연거푸 실행될거기 때문에 안전장치로 만드는 함수입니다.
  • if (!observerRef.current) {...}: observerRef.current에 아무런 값이 없는 경우에만 new IntersectionObserver()로 인스턴스를 생성해줍니다.
  • return observerRef.current: 그리고 observerRef.currentreturn합니다.
  • [observerRef.current]: observerRef.current만을 감시해 해당 값이 변경될 때만 감지하도록 하겠습니다.

  • if (targetEl.current) getObserver().observe(targetEl.current): 이렇게 getObserver() 함수로 대체하면 함수는 매번 실행이 되지만,
    observerRef.current 값이 최초 null일 때, 딱 한번만 IntersectionObserver 이니셜라이징하고
    그 이후로는 이미 값이 들어와있기 때문에 만들어진 observerRef.current가 반환이 됩니다.
  • return () =&gt; { getObserver().disconnect() }: 컴포넌트가 제거될 때 실행되는 코드도 이렇게 바꿔줍니다.

Note
  • const newMsgs = await fetcher('get', '/messages', {params: {cursor: msgs[msgs.length - 1]?.id || ''}})
    무한스크롤 구현을 위해 3번째 인자로 params로 맨 마지막 메시지의 id 값을 넘기도록 하겠습니다.
    최초에는 마지막 메시지 값이 없기 때문에 위와 같이 ? 연산자를 활용해 작성합니다.
    이렇게하면 서버에 cursor 값이 빈값으로가거나 마지막 메시지의 id 값으로 가거나합니다.
    이렇게하면 서버쪽도 수정이 필요합니다.

  • useEffect(() =&gt; { getMessage(); }, []): 최초 MsgList 컴포넌트가 생성될 때 getMessage(); 함수를 호출합니다.

  • useEffect(() =&gt; { if (intersecting) getMessages() }, [intersecting]): intersecting 값을 감시하다가 바뀔 때마다 if (intersecting) getMessages() 코드를 실행합니다.
    intersecting값이 true일 때마다 getMessages 함수를 호출합니다.


서버쪽 라우터의 get 부분을 보겠습니다.

  • handler: ({query: {cursor = ''}}) =&gt; {...}: 왼쪽과 같이 cursor 값을 받아올 수 있습니다.
    cursor 값이 안넘어올 경우를 생각해서(최초에) 기본값으로 빈문자열을 넣습니다.

  • const fromIndex = msgs.findIndex(msg =&gt; msg.id === cursor) + 1;
    fromIndex 값부터 메시지 불러오기 위해서 위와 같이 넘겨받은 마지막 메시지의 id값인 cursor와 매칭되는 메시지의 index를 찾고, 거기에 +1을 해줍니다.
    최초에는 cursor 값에 아무것도 없어서 '' 빈문자열로 올테니까 findIndex 값으로 -1을 반환할 것입니다.
    즉, 최초엔 fromIndex 값이 0이 될겁니다.
    최초엔 0부터 시작을 하게되는 겁니다.
    만약 cursor값으로 들어온 것과 msg.id를 매칭시켜서 findIndex를 했든데 그 값이 19라면, (현재 20개가 보인다는 뜻)
    거기에 +1이 되어서 fromIndex값이 20이 될것입니다.
    그럼 20부터 불러오겠죠?

  • res.send(msgs.slice(fromIndex, fromIndex + 15)): fromIndex부터 15개씩 불러오도록 응답값을 설정합니다.

Note

처음 접속하면, 위와 같이 15개의 메시지 목록이 불러와지는 것을 볼 수 있습니다.
최초 cursor값에 아무것도 안들어가니깐 findIndex() 함수를 통해 반환되는 값은 -1이고 거기에 +1을 하니까
0fromIndex에 담기고, 거기서 +15만큼을 slice()해서 응답해줬기 때문에
이렇게 최초에 15개 메시지를 불러올 수 있는 것입니다.

Note

음.. 그런데 스크롤을 맨 아래로 내렸더니 MsgItem 컴포넌트가 아래에 더 생성되는 것이 아닌 그 다음 내용들로 교체가됩니다.
이는 저희가 원했던 작동방식이 아닙니다.

그런데 위와 같이 바꿔치기가 되는 것은 우리가 원했던바는 아닙니다.
그 아래에 붙어야되는데, 지금 새로 아예 교체가되었다는게 문제입니다.
이 부분을 수정을 해봅시다.
바꿔치기가 안되려면 getMessages 함수의 setMsgs를 수정하면 될거같습니다.

Note
  • setMsgs(newMsgs): 이렇게되어있어서 MsgItem 컴포넌트가 아래로 붙지않고 교체되었던겁니다.
  • setMsgs([...msgs, ...newMsgs]): 이렇게 기존 msgs 뒤에 newMsgs 내용을 붙여주는식으로 코드를 작성하면됩니다.

Note

그런데 처음 접속하면 위와 같이 getMessages 함수가 2번 실행됩니다.
이렇게 2번 실행되는 이유는 useEffect에 있습니다.
처음 MsgList 컴포넌트가 생성될 때 useEffect(() =&gt; { getMessages(); }, [])getMessages()가 실행됩니다.(여기서 1번)

처음 접속할 때 intersecting은 무조건 true입니다. (msgs가 없으니까 div가 화면에 보일테니까요)
그래서 useEffect(() =&gt; {if (intersecting) getMessages()})도 실행이 되는겁니다. (2번)

Note

그런데 생각해보니 useEffect(() =&gt; {getMessages()}, [])는 없어도 될거같습니다.
위와 같이 지웁니다.

Note

이제는 실제 요청이 딱 1번만 가는 것을 볼 수 있습니다.

Note

스크롤이 마지막에 다다를때마다 위와같이 getMessages 함수를 한번씩 실행합니다.

Note

그런데 이제 무한스크롤 기능은 잘 되는데 메시지 데이터 전체를 완전히 다 불러와서 더이상 불러올 메시지가 없을 때도
마지막 스크롤로가면 자꾸 데이터 요청을 보냅니다.
이 부분에 대한 안전장치만 달아두면 될거같습니다.

이 안전장치를 마련하기위해 마지막에 도달했을 때 응답으로 빈배열이 오는 것을 활용하면됩니다.


const [hasNext, setHasNext] = useState(true);

const getMessages = async () => {
    const newMsgs = await fetcher('get', '/messages', {params: {cursor: msgs[msgs.length - 1]?.id || ''}})
    if (newMsgs.length === 0) {
        setHasNext(false);
        return
    }
    setMsgs([...msgs, ...newMsgs])
}

useEffect(() => {
    if (intersecting && hasNext) getMessages()
}, [intersecting]) // intersecting 값 감시

Note

위와 같이 newMsgs.length0이면(빈배열이면) hasNextfalse로 바꾸고 return으로 실행을 중지시킵니다.
hasNext의 값이 false가 되었으므로 더이상 useEffect에서 intersecting 값이 바뀌어도 getMessages()를 호출하지 않습니다.

Note

위와 같이 마지막 스크롤에 도달을 여러번해도 요청은 딱 한번만가고 안갑니다.


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useRef, useState} from "react";
import {useRouter} from "next/router"; // localhost:3000/?userId=jay 에서 jay를 req.body로 넘겨주기 위해 불러온 라이브러리입니다.
import fetcher from "../fetcher.js";
import useInfiniteScroll from "../hooks/useInfiniteScroll";

// 이제 필요없어진 아래 코드들은 지우거나 주석처리합시다.
// const UserIds = ['roy', 'jay'];
// const getRandomUserId = () => UserIds[Math.round(Math.random())];

// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
// const originalMsgs = Array(50).fill(0).map((_, i) => ({
//     id: 50 - i,
//     userId: getRandomUserId(),
//     timestamp: 1234567890123 + (50 - i) * 1000 * 60, // 1분마다 하나씩
//     text: `${50 - i} mock text`
// }))

// [
//     {
//         id: 1,
//         userId: getRandomUserId(),
//         timestamp: 1234567890123,
//         text: "1 mock text"
//     }
// ]

const MsgList = () => {
    // 아래보시면 userId를 현재는 I <- 대문자로 받아오게 되어있는데, 소문자이든, 대문자이든 상관없게끔 바꿔보도록 하겠습니다.
    const {query} = useRouter();
    // 아래와 같이 userId 또는 userid 이렇게 바꿔주시면 queryString으로 소문자로 넘어와도 문제가 없이 잘 작동합니다.
    // 그리고 여기서 안전장치 하나를 걸어두는게 좋을 거 같습니다.
    // userId가 '' 빈문자열일 경우, 어떻게 작동해야될지에 대해서도 작성하는 것이 좋을 거 같습니다.
    // userId가 '' 빈문자열일 경우, CRUD 모두 아무것도 하지 않게하는 것이 좋을거 같습니다.
    // 이 부분에 대해서는 서버에서 처리를 해보겠습니다.
    const userId = query.userId || query.userid || '';
    // const {query: {userId = ''}} = useRouter(); // localhost:3000/?userId=jay 기능 구현을 위해 왼쪽과 같이 작성합니다.
                                                // next/router로 query를 받아오고 그 안에 들어있는 userId를 꺼냅니다.
                                                // 만약 없을 경우를 대비해 기본값으로 빈문자열을 부여합니다.

    // 지금 userId를 받아온 현상태에서 MsgItem으로 넘겨주시면 MsgItem에서도 수정/삭제버튼 노출 여부를 결정할 수 있겠죠?

    const [msgs, setMsgs] = useState([]); // 처음에 빈배열이 오도록 하겠습니다.
    const [editingId, setEditingId] = useState(null);
    // 메시지 데이터 전부를 불러왔을 때 스크롤 마지막으로 갈때마다 메시지 요청을 보내는거 방지하기 위한 코드.
    // 기본값은 true로 넣어주면 될거같습니다.
    // 그리고 이걸 통해서 요청을 할지말지를 판단하면 되겠습니다.
    const [hasNext, setHasNext] = useState(true);
    // 일단 fetchMoreEl에 useRef로 null을 넣어놓겠습니다.
    const fetchMoreEl = useRef(null);
    // intersecting true가 넘어오는지 false가 넘어오는지를 intersecting 옵저버를 쓰는 hook을 이용해서 해보도록 하겠습니다.
    // useInfiniteScroll 함수에 fetchMoreEl를 인자값으로 넘겨주면서, useInfiniteScroll에 대한 판단을 직접 해준다음에
    // 화면상에 fetchMoreEl가 노출되었을 때, intersecting이 true고 그렇지않으면 false가 되게끔 해보겠습니다.
    const intersecting = useInfiniteScroll(fetchMoreEl)

    // POST 기능에 해당하는 onCreate 메소드는 useEffect 내부에서 쓰는 것이 아니기 때문에 아래와 같이 async를 바로 적용할 수 있습니다.
    const onCreate = async text => {
        // await로 newMsg를 바로 만듭니다.
        // fetcher 메소드를 호출하고 첫번째 인자로 post, 두번째 인자로 url, 세번째 인자로 test와 userId를 보냅니다.
        // server/src/routes/messages.js에서 post 관련 정의하신대로 body에 text와 userId가 들어오도록 정의가 되어있습니다.
        // 하지만 아래 userId는 현재로썬 없는 상태입니다.
        // 이걸 한번 만들어봅시다.
        // 간단한 SNS 토이프로젝트이기 때문에 로그인 구현 없이, url 상의 쿼리로 userId를 넘길 수 있게끔 하겠습니다.
        // localhost:3000/?userId=jay <- 이런식으로 넘겼을 땐 jay에 관해서만 수정 삭제가 이뤄지고 roy는 수정/삭제 버튼이 안 나타나도록 하고
        // 새로운 글을 입력할 때, localhost:3000/?userId=jay 이렇게 입력받은 jay가 body로 넘어갈 수 있게끔,
        // 그렇게 하기 위해서는 next router가 필요합니다.
        const newMsg = await fetcher('post', '/messages', {text, userId})
        if (!newMsg) throw Error('something wrong') // 여기도 안전 장치를 둡시다.
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    // 바뀐 text값, 그리고 어떤 text인지 알기위해서 id 값도 받아야합니다.
    // 이번엔 update 기능(PUT)을 수정하겠습니다.
    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong') // 안전장치. newMsg가 없다면 아무것도 하지 않도록 설정

        // 아래처럼 state 안에서 기존 데이터를 받아오게끔하면 좀 더 안정적입니다.
        // 그래서 setState를 아래처럼 함수형으로 사용하는 것을 추천한다고합니다.
        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            // 아래 3번째 인자만 newMsg로 수정하면 됩니다.
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }

    // delete는 text가 필요없고 id만 있으면됩니다.
    // delete도 구현해보도록 하겠습니다. 마찬가지로 async로 하겠습니다.
    const onDelete = async (id) => {
        // 서버에 보시면 delete에 대한 응답을 삭제된 id 값을 넘겨주도록 되어있었기 때문에
        // 변수명을 receivedId라고 하겠습니다.
        // 그래서 아래와 같이 params: { userId } 이렇게 넘겨야합니다.
        // 이렇게 넘기는 것이 localhost:3000/messages/${id}?userId=${userId} <- 이렇게 넘기는 것과 같습니다.
        // 즉, 아래와 같이 params: {userId}로 보내주거나
        // const receivedId = await fetcher('delete', `/messages/${id}?userId=${userId}`) <- 이렇게 작성하면됩니다.
        // 일단 저희는 params로 보내보겠습니다.
        // 이렇게 params로 보내줬지만 실제로는 queryString이 되어서 보내지게 되는 거죠.
        const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})

        setMsgs(msgs => {
            // 그리고 아래서 msg.id와 receivedId와 비교를 하면 되겠죠.
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
            // findindex로 일치하는 값이 없으면 -1 반환
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1) // update와의 차이점, splice를 해준 다음에 그 자리에 새로운 값을 안 넣어주면된다.
            return newMsgs;
        })
    }

    // update가 완료되었다는 것을 알려줍니다.
    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        // 무한스크롤 구현을 위해 3번째 인자로 params로 맨 마지막 메시지의 id 값을 넘기도록 하겠습니다.
        // 아래 변수명 msgs가 state와 겹치기 때문에 newMsgs로 바꾸겠습니다.
        // 이렇게 작성하면 cursor 값에 마지막 메시지의 id가 들어갈겁니다.
        // 그런데 최초에는 마지막 메시지 값이 없기 때문에 아래와 같이 ? 연산자를 활용해 작성하겠습니다.
        // cursor는 빈값으로 가거나, 마지막 메시지의 Id 값으로 가거나..
        // 이렇게 하면 서버쪽도 수정이 필요할겁니다.
        const newMsgs = await fetcher('get', '/messages', {params: {cursor: msgs[msgs.length - 1]?.id || ''}})
        // newMsgs.length가 0이면 hasNext를 false로 바꾸고 return
        if (newMsgs.length === 0) {
            setHasNext(false);
            return
        }
        setMsgs([...msgs, ...newMsgs])
    }
    // useEffect(() => {
    //     getMessages();
    // }, [])

    useEffect(() => {
        // intersecting 값이 true일 때만 getMessages() 함수 호출
        // hasNext도 true일 때 getMessages() 함수 호출
        if (intersecting && hasNext) getMessages()
    }, [intersecting]) // intersecting 값 감시

    return (
        <>
            {userId && <MsgInput mutate={onCreate}/>}
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)} // onDelete가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
                                           isEditing={editingId === x.id}
                                           myId={userId}
                    />)
                }
            </ul>
            {/*무한 스크롤 기능을 구현하기 위해선 아래 요소가 필요합니다.*/}
            {/*아래 요소가 화면상에 나타났을 때, 다음 데이터를 불러와라 라고 요청하는 이 부분을 넣어줘야됩니다.*/}
            <div ref={fetchMoreEl} />
        </>
    )
}

export default MsgList;


// server/src/routes/messages.js
import {v4} from "uuid";
import {readDB, writeDB} from "../dbController.js";

const getMsgs = () => readDB('messages') // 중복을 방지하기위한 코드입니다.
const setMsgs = data => writeDB('messages', data) // 중복을 방지하기위한 코드입니다.
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        // 아래와 같이 cursor 값을 받아올 수 있습니다. 기본값은 '' 빈문자열로 설정합니다.
        handler: ({query: {cursor = ''}}, res) => {
            // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다. 다 똑같으니까 이거를 함수로 바꿔봅시다.
            const msgs = getMsgs();
            // fromIndex 값부터 메시지 불러오기 위해서 아래와 같이 작성
            // 그럼 최초에는 cursor 값에 아무것도 없어서 '' 빈문자열로 올테니까 findIndex 값으로 -1을 반환할 것이고
            // 그럼 최초에는 fromIndex 값이 0이 되겠죠?
            // 최초에는 0부터 시작을 하게되는 겁니다.
            // 만약 cursor 값으로 19가 넘어왔다 (현재 20개가 보인다는 뜻)
            // 그럼 fromIndex는 20이 될겁니다.
            // 그럼 20부터 불러오겠죠
            const fromIndex = msgs.findIndex(msg => msg.id === cursor) + 1
            // 그럼 아래와같이 send를 할 때 slice를 해서 보냅니다.
            // fromIndex부터 15개씩 불러오고 싶다면? fromIndex + 15 이렇게.
            res.send(msgs.slice(fromIndex, fromIndex + 15))
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        method: 'get',
        route: '/messages/:id',
        handler: ({params: {id}, res}) => { // id를 직접 받아오기 때문에 에러가날 가능성이 있으므로 여기도 마찬가지로 에러 처리를 해줍니다.
            try {
                const msgs = getMsgs();
                const msg = msgs.find(m => m.id === id) // 이번엔 findIndex 메소드가 아니라 find 메소드를 사용합니다.
                if (!msg) throw Error('not found')
                res.send(msg) // msg를 send해줍니다.
            } catch (err) {
                res.status(404).send({ error: err })
            }
        }
    },
    { // CREATE MESSAGE
        method: 'post',
        route: '/messages',
        // POST는 새글을 등록하는겁니다.
        // 첫번째(request) 인자에는 body, params, query가 있습니다. 그 중에서 body를 사용하게됩니다.
        // body는 새글이 등록된 text가 들어있을거고 그리고 userId도 들어있습니다.
        handler: ({body}, res) => {
            try {
                // GET은 어차피 userId 안보내니깐 상관없고 POST에서 아래와 같이 작성해줍니다.
                // UPDATE나 DELETE도 이미 userId 관련 처리가 되어있으니 POST만 처리해주면됩니다.
                // body.userId가 없으면 에러를 발생시킵니다.
                if (!body.userId) throw Error('no userId');

                // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
                const msgs = getMsgs();
                const newMsg = {
                    id: v4(), // uuid의 v4 버전의 id를 만들겠다는 뜻입니다.
                    text: body.text,
                    userId: body.userId,
                    timestamp: Date.now(),
                }
                msgs.unshift(newMsg) // 새글을 배열의 제일 앞에 넣어줍니다.
                // writeDB('messages', msgs) // 그리고 DB에 기록합니다. // 이 코드도 자주 사용하므로 위에 함수로 뺍니다.
                setMsgs(msgs);
                res.send(newMsg) // 그리고 응답은 업데이트된 메시지만 보내면 될겁니다.
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
    { // UPDATE MESSAGE
        method: 'put',
        route: '/messages/:id', // <- UPDATE는 이렇게 실제 id를 지정해서 요청을 보내는겁니다.
        // UPDATE는 body에 변경된 text가 들어올거고 params 안에 id가 들어오게됩니다.
        // 이런 부분은 여러분들이 첫번째 인자(request)를 콘솔에 출력해보시면 확인하실 수 있으실겁니다.
        handler: ({body, params: {id}}, res) => {
            // UPDATE 요청은 위의 :id로 실제 id로 요청을 보내는거다보니까 클라이언트에선 id가 나와있는데,
            // 실제 서버에선 없는 경우, 혹은 그 반대인 경우,
            // 이런식으로 서버와 클라이언트간 싱크가 맞지 않아서 오류가날 가능성이 없진 않을겁니다.
            // 그래서 그런 경우에 대한 안전대비책을 해놓고 가겠습니다.
            try {
                // const msgs = readDB('messages') // 이거는 똑같이 다 들어갑니다.
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id) // targetIndex 찾는 방법은 똑같습니다.
                if (targetIndex < 0) throw '메시지가 없습니다.'
                if (msgs[targetIndex].userId !== body.userId) throw '사용자가 다릅니다.'

                const newMsg = {
                    ...msgs[targetIndex], // 기존내용을 다 담고
                    text: body.text, // text만 새로 담으면됩니다.
                }
                msgs.splice(targetIndex, 1, newMsg)
                setMsgs(msgs)
                res.send(newMsg) // 새로 변경된 메시지를 send
            } catch (err) {
                res.status(500).send({error: err}) // error가 날 경우 status를 500으로 지정하고 err 메시지를 띄웁니다.
            }
        }
    },
    { // DELETE MESSAGE
        // 서버에서 보시면 delete에 대한 응답이 아래처럼 id만 넘겨주도록 되어있었죠?
        method: 'delete',
        route: '/messages/:id',
        // queryString으로 오기 때문에 아래선 query: {userId}로 받아야됩니다.
        handler: ({params: {id}, query: {userId}}, res) => {
            try {
                const msgs = getMsgs();
                const targetIndex = msgs.findIndex(msg => msg.id === id)
                if (targetIndex < 0) throw '메시지가 없습니다.'
                // 아래도 수정해줍시다.
                if (msgs[targetIndex].userId !== userId) throw '사용자가 다릅니다.'

                msgs.splice(targetIndex, 1)
                res.send(id) // DELETE 성공했을 때, id만 넘겨주면될겁니다. 이 id가 지워졌어요 라는 메시지를 던져주는겁니다.
            } catch (err) {
                res.status(500).send({error: err}) // 실패하였을 땐 에러메시지를 던져줍니다.
            }
        }
    },
]

export default messagesRoute


// client/hooks/useInfiniteScroll.js
import {useCallback, useEffect, useRef, useState} from "react";

const useInfiniteScroll = targetEl => {
    const observerRef = useRef(null);
    const [intersecting, setIntersecting] = useState(false)
    // const observer = new IntersectionObserver(entries => setIntersecting(
    //     // entries는 배열형태라는데.. 뭐가 배열형태로 들어오는거지?
    //     // 여튼 배열 형태로 들어오는 entries 중에 isIntersecting이 true인게 하나라도 있으면? true로 지정하고
    //     // 하나도 없으면 false로 지정.
    //     entries.some(entry => entry.isIntersecting)
    // ))

    // useInfiniteScroll 함수가 MsgList에서 한번만 호출되는 것이 아니라 연거푸 실행될거기 때문에 아래와 같은 안전장치가 필요합니다.
    const getObserver = useCallback(() => {
        // observerRef.current가 false라면, 아래 내용을 실행시킵니다.
        // 즉, observerRef.curret에 아무 내용이 없는 경우에 한해서 아래 new IntersectionObserver(...)를 넣어주는 겁니다.
        if (!observerRef.current) {
            observerRef.current = new IntersectionObserver(entries => setIntersecting(
                entries.some(entry => entry.isIntersecting)
            ))
        }
        // observerRef.current가 있을 경우엔 아래와 같이 return하면 되겠습니다.
        return observerRef.current
    }, [observerRef.current]) // observerRef.current가 변경될 때만 감지하도록 하겠습니다.

    useEffect(() => {
        // targetEl이 있다면 해당 targetEl을 계속 감시.
        // (컴포넌트가 생성될 때)
        // 이렇게 getObserver() 함수로 대체하면, 함수는 매번 실행이되지만,
        // observerRef.current 값이 최초에 null일 때, 딱 한번만 IntersectionObserver 이니셜라이징하고
        // 이후로는 이미 값이 들어와있기 때문에 만들어진 observerRef.current가 반환이 됩니다.
        if (targetEl.current) getObserver().observe(targetEl.current)
        // if (targetEl.current) observer.observe(targetEl.current)

        // useInfiniteScroll이 더이상 화면상에 존재하지 않을 때, (컴포넌트가 사라질 때)
        // 아래와 같이 disconnect 해주면됩니다.
        return () => {
            getObserver().disconnect()
            // observer.disconnect()
        }
    }, [targetEl.current]) // targetEl.current 값이 변경될 때마다 이 동작이 실행되도록.

    // 그리고 intersecting 값을 return해주면 됩니다.
    return intersecting
}

export default useInfiniteScroll

3.4 서버사이드 렌더링

이번에는 서버사이드 렌더링을 구현해보도록 하겠습니다.


// 서버사이드 렌더링을 위해서 `client/pages/_app.js` 파일을 만듭니다.  
// 아래와 같이 코드를 작성합니다.
import './index.scss'

// next가 서버사이드 렌더링을 하기위해 필요한 컴포넌트입니다.
// 그래서 아래와 같은 기본 공식이 있습니다.
// 기본 공식 그대로 코드를 작성하시면됩니다.
const App = ({ Component, pageProps }) => <Component {...pageProps} />

// ctx는 context의 줄임말입니다.
App.getInitialProps = async ({ctx, Component}) => {
    // 각 컴포넌트별로 getInitialProps가 정의되어있을 때, ctx를 넘겨서
    const pageProps = await Component.getInitialProps?.(ctx)
    // 그거에대한 pageProps를 return합니다.
    // 이 pageProps를 가지고 각각의 컴포넌트를 구성하는 형태인 것 같습니다.
    return { pageProps }
}

export default App


// client/next.config.js 파일을 생성합니다.
// 아래와같이 './pages' 경로를 설정해줍니다.
const path = require('path')

module.exports = {
    sassOptions: {
        includePaths: [path.resolve(__dirname, './pages')],
    },
}


// client/pages/index.js
import MsgList from "../components/MsgList";
// 이 index 파일에서 데이터를 불러오기 위해 fetcher를 불러옵니다.
import fetcher from "../fetcher";

// 아래와 같이 return하면 Home에서 smsgs 프롭을 받아올 수 있습니다.
const Home = ({smsgs, users}) => (
    <>
        <h1>SIMPLE SNS</h1>
        <MsgList smsgs={smsgs} users={users} />
    </>
)

export const getServerSideProps = async () => {
    // 여기서는 cursor 안 보내도 된다. 
    // 어차피 최초 렌더링때 불러오는 것이므로 cursor 값을 마지막 메시지의 id로 설정해도 
    // 서버에선 cursor 값을 '' 빈문자열로 받기 때문이다.
    const smsgs = await fetcher('get', '/messages')
    const users = await fetcher('get', '/users')
    return {
        // 이렇게 return하면
        props: {smsgs, users}
    }
}

export default Home;


// client/components/MsgList.js
const MsgList = ({smsgs, users}) => {
    // ...
    const [msgs, setMsgs] = useState(smsgs); // 처음엔 msgs에 빈배열을 할당했었다. 
                                             // 하지만 지금은 서버사이드 랜더링 방식으로 수정해서 smsgs 값을 서버로부터 받아오므로 msgs에 받아온 smsgs 값을 초기값으로 넣는다.
    // ...
}

Note

위에 보면 Home 컴포넌트에 smsgs props 값이 들어와있는 것을 볼 수 있습니다.
그리고 그것이 MsgList 컴포넌트로 전달되어 MsgList 컴포넌트에도 마찬가지로 smsgs 값이 들어와있는 것을 볼 수 있습니다.

Note

네트워크 요청을 보시면 이제 최초 접속시 get 요청이 가지 않습니다.
그 이유는 intersectingfalse가 되어 get요청이 가지 않는 것입니다.
msgs 값이 smsgs로 설정되기 때문에 화면에 메시지들이 최초에 15개가 보여 intersecting값이 true가 아니라 false가 되는 것입니다. (스크롤 감지하는 div요소가 안보이니깐)

즉, 처음에 메시지 15개가 들어왓으므로 화면에 IntersectionObserver로 감시하는 div 요소가 안 보이기 때문에 false로 설정됩니다.
그렇기 때문에 처음에 get 요청을 보내지 않습니다.

Note

그런데 위와 같이 작동하는건 신기하네..
이것이 next.js의 역할인건가..?
여튼 서버사이드 렌더링을 통해 처음에 if (intersecting &amp;&amp; hasNext) getMessages() 실행을 안한다.

Note

스크롤을 아주 끝까지 내릴 때부터 messages 요청이 들어갑니다.

Note

처음에 msgs 값을 빈배열로 설정했을 땐

Note

처음에 msgs 값을 빈배열로 설정했을 땐, 처음에 접속할 때 위와 같이 서버에 GET 요청을 보내어 위 왼쪽 화면이 깜빡 거릴 것입니다.
index.js 파일의 Home 컴포넌트를 렌더링한 후에 서버에 요청을 보내 응답받고 다시 MsgList 컴포넌트가 렌더링이 되어서 화면 깜빡임이 생기는 것입니다.

Note

하지만 서버사이드 렌더링을통해 처음에 위와 같이 smsgs 메시지들을 넘겨주면

Note

처음 접속시 메시지들(데이터들)과 함께 바로 렌더링되므로 화면 깜빡임 현상이 없습니다.

Note

const [msgs, setMsgs] = useState([]);
더 확인을 잘 하기 위해서 빈배열, 그리고 아래쪽에 console.log('render')를 찍어보면,

Note

최초에 render가 4번이나 찍힌 것을 볼 수 있습니다.
어떤거 때문에 이렇게 렌더링이 많이 일어났을까요?
state 값이 바뀔 때마다 const MsgList = () =&gt; {} 이 코드 전체가 다시 실행되나봅니다.
그래서 render가 4번이나 찍힌 것이고.. 즉, 4번이나 렌더링이 일어났다는 뜻..

그런데 왜 4번이나!?
이유가 뭐지?

Note

intersecting, hasNext 값들의 변화
setMsgs()로 인한 msgs의 변화
등등 이런 것들로 인해 render가 많이 발생한 것 같습니다.

강사님도 확실힌 모른다는 것처럼 넘어가심.. 아니면 설명을 자세하게하기 귀찮으셨나..


// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useRef, useState} from "react";
import {useRouter} from "next/router";
import fetcher from "../fetcher.js";
import useInfiniteScroll from "../hooks/useInfiniteScroll";

const MsgList = ({smsgs, users}) => {
    const {query} = useRouter();
    const userId = query.userId || query.userid || '';

    // 처음에 msgs가 빈배열이라면
    // 처음 MsgList가 렌더링되고 (랜더링 1번째)
    // 아래 useEffect의 getMessages() 함수 실행으로인해 msgs 값이 바뀌므로 다시 렌더링되고 (랜더링 2번째)
    // ... 그 다음엔 뭐지.. 왜 새로고침할 때마다 아래가 빈배열이면 랜더링이 4~5번 실행되는걸까? 잘은 모르겠다..
    // 강사님도 정확히 뭐때문에 4~5번 실행되었다고 말씀은 안해주심..
    const [msgs, setMsgs] = useState(smsgs);
    const [editingId, setEditingId] = useState(null);
    const [hasNext, setHasNext] = useState(true);
    const fetchMoreEl = useRef(null);
    const intersecting = useInfiniteScroll(fetchMoreEl)

    const onCreate = async text => {
        const newMsg = await fetcher('post', '/messages', {text, userId})
        if (!newMsg) throw Error('something wrong') 
        setMsgs(msgs => ([newMsg, ...msgs]))
    }

    const onUpdate = async (text, id) => {
        const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
        if (!newMsg) throw Error('something wrong') 

        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === id);
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1, newMsg);
            return newMsgs;
        })
        doneEdit();
    }

    const onDelete = async (id) => {
        const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})

        setMsgs(msgs => {
            const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
            if (targetIndex < 0) return msgs;
            const newMsgs = [...msgs]
            newMsgs.splice(targetIndex, 1)
            return newMsgs;
        })
    }

    const doneEdit = () => setEditingId(null)

    const getMessages = async () => {
        const newMsgs = await fetcher('get', '/messages', {params: {cursor: msgs[msgs.length - 1]?.id || ''}})
        if (newMsgs.length === 0) {
            setHasNext(false);
            return
        }
        setMsgs([...msgs, ...newMsgs])
    }

    useEffect(() => {
        if (intersecting && hasNext) getMessages()
    }, [intersecting]) 

    console.log('render')

    return (
        <>
            {userId && <MsgInput mutate={onCreate}/>}
            <ul className='messages'>
                {
                    msgs.map(x => <MsgItem key={x.id}
                                           {...x}
                                           onUpdate={onUpdate}
                                           onDelete={() => onDelete(x.id)}
                                           startEdit={() => setEditingId(x.id)}
                                           isEditing={editingId === x.id}
                                           myId={userId}
                                           user={users[x.userId]}
                    />)
                }
            </ul>
            <div ref={fetchMoreEl} />
        </>
    )
}

export default MsgList;

Note

다시 const [msgs, setMsgs] = useState([]) 여기 부분을
const [msgs, setMsgs] = useState(smsgs)로 바꾸고 확인해보면

Note

새로고침을 해보면 render가 한번만 발생합니다.
msgs가 처음부터 있으니 최초 접속시 변경되는 값이 없기 때문입니다. (intersecting, hasNext, setMsgs` 등등..)

Note

그런데 스크롤을 맨 아래로 내리니깐 render가 3번이나 실행되네요?
흠 여튼 이러한 상황이기 때문에 최초에 render가 한번만 일어나는 것이 아무래도 여러모로 성능면에서 유리합니다.
그래서 서버사이드 랜더링이 이런 장점이 있음을 말씀드립니다.

어쨌든 이러한 상황이기 때문에 최초에는 render가 한번만 일어나는 것이 아무래도 여러모로 성능면에서 유리합니다.
그래서 서버사이드 랜더링이 이런 장점이 있음을 말씀드립니다.

기왕 프론트엔드 확인하는 김에 조금 더 아쉬운 점을 보도록 하겠습니다.

Note

메시지마다 userId가 노출되고있는데, 한글로 nickname도 만들어놨잖아요?
이를 userId가 아니라 nickname으로 랜더링되도록 해보겠습니다. 서버사이드 랜더링 하는김에.

const Home = ({smsgs, users}) =&gt; (...)
&amp;lt;MsgList smsgs={smsgs} users={users} /&amp;gt;
const users = await fetcher('get', '/users');return { props: {smsgs, users} }

위와 같이 users 정보도 여기에 들어올 수 있게 해보겠습니다.


const MsgList = ({smsgs, users}) =&gt; {}
그리고 MsgList에서도 users를 추가합니다.

Note

위와 같이 Home을 보시면 users 정보가 들어와있는걸 보실 수 있습니다.

Note

위와 같이 하면 MsgItem 컴포넌트에
user={users[x.userId]}
이렇게 propsMsgItemuser 정보를 넘겨줄 수 있다.
그러면 MsgItem 컴포넌트 각각에 user 정보가 들어있는 상태가 될겁니다.

Note

위와 같이 MsgItem 컴포넌트에 user props를 받도록 인자값을 수정해줍니다.
그리고 {user.nickname}{' '} 이렇게 설정해주면 화면에 nickname으로 나오게 됩니다.

Note

위와 같이 nickname으로 나오는 것을 보실 수 있습니다.

Note

userId jay로 로그인해서 글을 입력하면 nickname 제이로 글이 등록됩니다.

Note

userId roy로 로그인해서 글을 입력하면 nickname 로이로 글이 등록됩니다.

다음 시간부턴 REST API로 만든 것들을 GraphQL로 바꿔보도록 하겠습니다.