1 Client - 기본기능 구현
source: categories/study/react_restapi_graphql/react_restapi_graphql2.md
1.1 (공지) 폰트 이슈
- FiraCode(폰트) 링크입니다. https://github.com/tonsky/FiraCode
1.2 환경 세팅
npm install -g yarn
yarn init -y
위와 같이 yarn
을 설치하고 yarn init -y
를 실행하면 아래 package.json
파일이 생성됩니다.
{
"name": "react-study2",
"version": "1.0.0",
"main": "index.js",
"license": "MIT"
}
package.json
에 다음과 같은 내용을 추가해줍니다.
{
"name": "react-study2",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": [
"client",
"server"
]
}
private
를 true
로 주는 이유는 workspaces
를 이용하려면 private
를 true
로 줘야된다는 설정이 있기 때문입니다.
그리고 workspace
로 client
랑 server
라는 워크스페이스를 만들도록 하겠습니다.
그리고 이 워크스페이스를 각각 실행하는 명령어로 scripts
를 지정해주겠습니다.
{
"name": "react-study2",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"private": true,
"workspaces": [
"client",
"server"
],
"scripts": {
"client": "yarn workspace client start",
"server": "yarn workspace server start"
}
}
위와 같이 작성해주시면 되겠습니다.
그럼 위의 워크스페이스에 지정해준 명칭 그대로(client
, server
) 폴더를 만들겠습니다.
root_folder/
|-- client/
|-- server/
|-- package.json
server
폴더는 나중에보기로하고 지금은 client
폴더에서 프론트엔드 진영에서 컴포넌트들을 다 생성을 하고 프론트엔드 안에서만 동작을하는 목(mock) 데이트를 활용해서 동작하는 것까지 구현을 해보도록 하겠습니다.
터미널에서 client
폴더로 이동하겠습니다.
그리고 다시 yarn init -y
를 해줍니다.
cd client
yarn init -y
root_folder/
|-- client/
| `-- package.json
|-- server/
|-- package.json
그럼 client
폴더 안에 package.json
파일이 생성된걸 확인할 수 있습니다.
이 상태에서 패키지들을 설치하겠습니다.
yarn add react react-dom next node-sass @zeit/next-sass axios
@zeit/next-sass
: next.js
라이브러리에서 sass
를 쓰기위한 라이브러리이다.
{
"name": "client",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@zeit/next-sass": "^1.0.1",
"axios": "^0.21.3",
"next": "^11.1.2",
"node-sass": "^6.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
}
}
dependencies
는 이정도이고 이번엔 devDependencies
를 설치해보겠습니다.
yarn add --dev webpack@4
next.js
에서 webpack 5버전
은 문제가 좀 있다라는 메시지를 몇번 봤기 때문에 4버전을 설치합니다.
{
"name": "client",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@zeit/next-sass": "^1.0.1",
"axios": "^0.21.3",
"next": "^11.1.2",
"node-sass": "^6.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"webpack": "4"
}
}
지금부터 본격적인 컴포넌트 생성을 해보도록 하겠습니다.
next.js
에서 기반이 되는 폴더가 pages
폴더입니다.
이 pages
폴더는 반드시 있어야됩니다.
root_folder/
|-- client/
| |-- pages/
| `-- package.json
|-- server/
|-- package.json
그리고 pages
폴더 안에 index.js
파일이 반드시 있어야됩니다.
root_folder/
|-- client/
| |-- pages/
| `-- index.js
| `-- package.json
|-- server/
|-- package.json
그리고 index.js
파일에 다음과 같이 Home
컴포넌트를 작성해줍니다.
// client/pages/index.js
const Home = () => (
<>
<h1>SIMPLE SNS</h1>
</>
)
export default Home;
그리고 client/package.json
에 scripts
에 명령어를 추가하도록 하겠습니다.
{
"name": "client",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@zeit/next-sass": "^1.0.1",
"axios": "^0.21.3",
"next": "^11.1.2",
"node-sass": "^6.0.1",
"react": "^17.0.2",
"react-dom": "^17.0.2"
},
"devDependencies": {
"webpack": "4"
},
"scripts": {
"start": "next"
}
}
다시 루트 폴더로 이동해서 yarn run client
명령어를 실행합니다.
cd ../
yarn run client
yarn run v1.22.10
$ yarn workspace client start
$ next
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
event - compiled successfully
Attention: Next.js now collects completely anonymous telemetry regarding usage.
This information is used to shape Next.js' roadmap and prioritize features.
You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
https://nextjs.org/telemetry
위와 같이 3000번 포트에서 화면을 확인할 수 있다는 메시지가 나옵니다.
화면을 확인해보시면
위와 같이 아까 만든 페이지가 뜨는 것을 확인하실 수 있습니다.
1.3 (추가) Next.js 11 대응
Next.js
버전이 11로 업데이트됨에 다라 환경세팅에 약간의 변동사항이 있습니다.
하단 내용을 참고해주세요.
바뀐 내용은 다음과 같습니다.
1.3.1 npm package 설치
// == before ==
yarn add react react-dom next node-sass @zeit/next-sass axios
yarn add --dev webpack@4
// == after ==
yarn add react react-dom next sass axios
yarn add --dev webpack
1.3.2 next.config.js
// == before ==
const withSass = require('@zeit/next-sass')
module.exports = withSass()
// == after ==
const path = require('path')
module.exports = {
sassOptions: {
includePaths: [path.resolve(__dirname, './pages')],
},
}
root_folder/
|-- client/
| |-- pages/
| `-- index.js
| `-- next.config.js
| `-- package.json
|-- server/
|-- package.json
1.3.3 client/package.json
{
"name": "client",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"axios": "^0.21.3",
"next": "^11.1.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"sass": "^1.39.0"
},
"devDependencies": {
"webpack": "^5.52.0"
},
"scripts": {
"start": "next"
}
}
위와 같이 수정된 상태에서 아래와 같이 실행하면 동일하게 실행될 것이다.
cd ..
yarn run client
yarn run v1.22.10
$ yarn workspace client start
$ next
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
event - compiled successfully
1.4 목록뷰 구현하기
본격적으로 메시지 리스트를 보여주는 클라이언트 컴포넌트를 만들어보겠습니다.
components
라는 폴더를 만듭니다.
root_folder/
|-- client/
| |-- components/
| `-- MsgList.js
| |-- pages/
| `-- index.js
| `-- next.config.js
| `-- package.json
|-- server/
|-- package.json
// client/components/MsgList.js
const MsgList = () => (
<ul className='messages'>
</ul>
)
export default MsgList;
이 상태에서 MsgItem.js
파일을 만들어봅시다.
root_folder/
|-- client/
| |-- components/
| `-- MsgItem.js
| `-- MsgList.js
| |-- pages/
| `-- index.js
| `-- next.config.js
| `-- package.json
|-- server/
|-- package.json
// client/components/MsgItem.js
const MsgItem = ({
userId,
timestamp,
text
}) => (
<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>
{text}
</li>
)
export default MsgItem
그리고 다시 MsgList.js
를 아래와 같이 수정해줍니다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];
// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
const msgs = Array(50).fill(0).map((_, i) => ({
id: i + 1,
userId: getRandomUserId(),
timestamp: 1234567890123 + i * 1000 * 60, // 1분마다 하나씩
text: `${i + 1} mock text`
}))
// [
// {
// id: 1,
// userId: getRandomUserId(),
// timestamp: 1234567890123,
// text: "1 mock text"
// }
// ]
const MsgList = () => (
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id} {...x} />)
}
</ul>
)
export default MsgList;
// client/pages/index.js
import MsgList from "../components/MsgList";
const Home = () => (
<>
<h1>SIMPLE SNS</h1>
<MsgList />
</>
)
export default Home;
yarn run client
yarn run v1.22.10
$ yarn workspace client start
$ next
ready - started server on 0.0.0.0:3000, url: http://localhost:3000
info - Using webpack 5. Reason: Enabled by default https://nextjs.org/docs/messages/webpack5
event - compiled successfully
event - build page: /next/dist/pages/_error
wait - compiling...
event - compiled successfully
event - build page: /
wait - compiling...
event - compiled successfully
위와 같이 1분 단위로 목록이 나오는 것을 볼 수 있습니다.
랜덤하게 어떨 땐 roy
였다가 어떨 땐 jay
였다가..
시간도 1분 단위로 계속 증가하는 것이 보이죠?
채팅목록은 제일 최신 메시지가 제일 위로 와야되는데 순서가 반대로되었네요.
순서를 수정해봅시다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];
// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
const msgs = Array(50).fill(0).map((_, i) => ({
id: i + 1,
userId: getRandomUserId(),
timestamp: 1234567890123 + i * 1000 * 60, // 1분마다 하나씩
text: `${i + 1} mock text`
})).reverse();
// [
// {
// id: 1,
// userId: getRandomUserId(),
// timestamp: 1234567890123,
// text: "1 mock text"
// }
// ]
const MsgList = () => (
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id} {...x} />)
}
</ul>
)
export default MsgList;
아니면 아래와 같이 수정해줘도됩니다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];
// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
const msgs = 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 = () => (
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id} {...x} />)
}
</ul>
)
export default MsgList;
1.5 스타일 작성
root_folder/
|-- client/
| |-- components/
| `-- MsgItem.js
| `-- MsgList.js
| |-- pages/
| `-- index.js
| `-- index.scss
| `-- next.config.js
| `-- package.json
|-- server/
|-- package.json
/* client/pages/index.scss */
body {
margin: 0;
}
#__next {
margin: 10px;
}
h1 {
text-align: center;
margin: 20px 0;
}
.messages {
list-style: none;
padding: 0;
margin: 0;
&__input {
display: flex;
width: 80%;
max-width: 700px;
min-width: 400px;
margin: 0 auto 10px;
> * {
box-sizing: border-box;
}
textarea {
padding: 10px;
flex-grow: 1;
}
button {
margin-left: 5px;
width: 60px;
}
}
&__item {
position: relative;
margin: 10px 0;
padding: 15px 15px 5px;
border-radius: 5px;
border: solid 1px #aaa;
h3 {
font-size: 0.85em;
margin: 0 0 10px;
}
sub {
font-weight: normal;
margin-left: 5px;
vertical-align: baseline;
}
p {
margin: 10px 0;
}
&:hover .messages__buttons {
display: block;
}
.messages__input {
width: 100%;
}
}
&__buttons {
display: none;
position: absolute;
text-align: right;
right: 10px;
bottom: 10px;
}
}
1.6 새글쓰기 기능 구현 (Create)
root_folder/
|-- client/
| |-- components/
| `-- MsgItem.js
| `-- MsgList.js
| |-- pages/
| `-- _app.js
| `-- index.js
| `-- index.scss
| `-- next.config.js
| `-- package.json
|-- server/
|-- package.json
// 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
yarn run client
// client/next.config.js
const path = require('path')
module.exports = {
sassOptions: {
includePaths: [path.resolve(__dirname, './pages')],
},
}
화면에 보이는 것까지 성공을 했는데, 이제는 메시지를 직접 입력하고 입력한 메시지가 화면에 출력되는걸 구현해볼겁니다.
root_folder/
|-- client/
| |-- components/
| `-- MsgInput.js
| `-- MsgItem.js
| `-- MsgList.js
| |-- pages/
| `-- _app.js
| `-- index.js
| `-- index.scss
| `-- next.config.js
| `-- package.json
|-- server/
|-- package.json
MsgInput.js
컴포넌트는 MsgItem.js
와 MsgList.js
에서 모두 사용할 것입니다.
MsgList.js
에선Create
용도로 사용할 것이고MsgItem.js
에선Update
용도로 사용할 것이다.
// client/components/MsgInput.js
import {useRef} from "react";
// MsgInput 컴포넌트는 MsgList, MsgItem 컴포넌트에서 사용할 것입니다.
// MsgList에선 Create의 목적으로
// MsgItem에선 Update의 목적으로 사용할 것입니다.
// 때문에 목적이 다르기 때문에 목적이 다른 메소드를 하나로 일괄해서 쓰기위해서 메소드 이름을 mutate라고 정의해보겠습니다.
const MsgInput = (mutate) => {
// react에서 form 데이터를 다루는 방식이 여러가지가 있겠지만,
// 많은 분들이 onChange 메소드 또는 onInput 메소드들을 쓰시는 걸로 알고 있습니다.
// 그런데 저는 이런 방법 말고 reference 방법을 이용하는 것을 선호합니다.
// 이것은 개인적인 선호도이기 때문에 꼭 이렇게하는 것이 좋다라는 것은 아닙니다.
// 이 방법이 마음에 안드시는 분들은 onChange 또는 onInput 등 으로 하셔도 될거같습니다.
const textRef = useRef(null); // 초기값이 없으면 안됩니다. null값이라도 설정해야됩니다.
const onSubmit = e => {
e.preventDefault();
e.stopPropagation();
const text = textRef.current.value; // text 변수에 textRef.current.value 값을 담습니다.
textRef.current.value = ''; // 그리고 바로 textRef의 값을 지워줍니다.
mutate(text); // 상위에서 mutate라는 메소드를 받아서 호출할 때 text를 인자값으로 넘겨줍니다. // Create용 메소드가 내려올 수도 있고, Update용 메소드가 내려올수도 있고~
}
return (
<form className='messages__input' onSubmit={onSubmit}>
<textarea
ref={textRef}
placeholder="내용을 입력하세요."
/>
<button type='submit'>완료</button>
</form>
)
}
export default MsgInput
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];
// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
const msgs = 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 onCreate = text => {
const newMsg = {
id: msgs.length,
userId: getRandomUserId(),
timestamp: Date.now(),
text: `${msgs.length} ${text}`,
}
msgs.unshift(newMsg);
}
return (
<>
<MsgInput mutate={onCreate}/>
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id} {...x} />)
}
</ul>
</>
)
}
export default MsgList;
아래와 같이 props 형태로 받아야됩니다.
// client/components/MsgInput.js
import {useRef} from "react";
// MsgInput 컴포넌트는 MsgList, MsgItem 컴포넌트에서 사용할 것입니다.
// MsgList에선 Create의 목적으로
// MsgItem에선 Update의 목적으로 사용할 것입니다.
// 때문에 목적이 다르기 때문에 목적이 다른 메소드를 하나로 일괄해서 쓰기위해서 메소드 이름을 mutate라고 정의해보겠습니다.
const MsgInput = ({mutate}) => {
// react에서 form 데이터를 다루는 방식이 여러가지가 있겠지만,
// 많은 분들이 onChange 메소드 또는 onInput 메소드들을 쓰시는 걸로 알고 있습니다.
// 그런데 저는 이런 방법 말고 reference 방법을 이용하는 것을 선호합니다.
// 이것은 개인적인 선호도이기 때문에 꼭 이렇게하는 것이 좋다라는 것은 아닙니다.
// 이 방법이 마음에 안드시는 분들은 onChange 또는 onInput 등 으로 하셔도 될거같습니다.
const textRef = useRef(null); // 초기값이 없으면 안됩니다. null값이라도 설정해야됩니다.
const onSubmit = e => {
e.preventDefault();
e.stopPropagation();
const text = textRef.current.value; // text 변수에 textRef.current.value 값을 담습니다.
textRef.current.value = ''; // 그리고 바로 textRef의 값을 지워줍니다.
mutate(text); // 상위에서 mutate라는 메소드를 받아서 호출할 때 text를 인자값으로 넘겨줍니다. // Create용 메소드가 내려올 수도 있고, Update용 메소드가 내려올수도 있고~
}
return (
<form className='messages__input' onSubmit={onSubmit}>
<textarea
ref={textRef}
placeholder="내용을 입력하세요."
/>
<button type='submit'>완료</button>
</form>
)
}
export default MsgInput
다시 인풋창에 텍스트를 입력하고 완료 버튼을 눌렀는데, 추가가 되어야할 거 같은데, 추가가 안됐습니다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
const UserIds = ['roy', 'jay'];
const getRandomUserId = () => UserIds[Math.round(Math.random())];
// 아래와 같이 배열 length가 50인 빈 배열을 만들어줍니다.
// 그리고 fill 메소드로 0값을 채워줍니다.
// fill 메소드로 값을 채워주기 전까진 map 메소드를 돌릴 수 없습니다.
const msgs = 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 onCreate = text => {
const newMsg = {
id: msgs.length,
userId: getRandomUserId(),
timestamp: Date.now(),
text: `${msgs.length} ${text}`,
}
msgs.unshift(newMsg);
console.log(msgs);
}
return (
<>
<MsgInput mutate={onCreate}/>
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id} {...x} />)
}
</ul>
</>
)
}
export default MsgList;
실제로 msgs
라는 배열에 newMsg
가 추가는 되었을 겁니다.
위와 같이 console.log
로 찍어보겠습니다.
찍어보면,
위와 같이 51개가 되었거든요?
id
값이 50으로 들어갔네요. (이건 이따 수정)
여튼 방금 추가된 text
가 추가된 것을 볼 수 있습니다.
이렇게 추가되었는데 화면상에는 렌더링이 안되는겁니다.
그 이유는 컴포넌트 입장에서는 msgs
가 변경되었다는 것을 감지하지 못한겁니다.
그래서 변경을 감지하게끔 하기 위해서는 이를 state
로 바꿔줄 필요가 있습니다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useState} from "react";
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(originalMsgs);
const onCreate = text => {
const newMsg = {
id: msgs.length + 1,
userId: getRandomUserId(),
timestamp: Date.now(),
text: `${msgs.length + 1} ${text}`,
}
setMsgs(msgs => ([newMsg, ...msgs]))
}
return (
<>
<MsgInput mutate={onCreate}/>
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id} {...x} />)
}
</ul>
</>
)
}
export default MsgList;
위와 같이 state
로 바꾸게되면 텍스트를 입력할 때마다 바로바로 화면에 렌더링되는 것을 볼 수 있습니다.
1.7 메시지 수정, 삭제 기능 구현 (Update, Delete)
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useState} from "react";
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(originalMsgs);
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;
})
}
return (
<>
<MsgInput mutate={onCreate}/>
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id}
{...x}
onUpdate={onUpdate}
startEdit={() => setEditingId(x.id)} // setEditingId가 실행될 때 id가 넘어와야하므로 왼쪽과 같이 작성해준다.
isEditing={editingId === x.id}
/>)
}
</ul>
</>
)
}
export default MsgList;
// client/components/MsgInput.js
import {useRef} from "react";
// MsgInput 컴포넌트는 MsgList, MsgItem 컴포넌트에서 사용할 것입니다.
// MsgList에선 Create의 목적으로
// MsgItem에선 Update의 목적으로 사용할 것입니다.
// 때문에 목적이 다르기 때문에 목적이 다른 메소드를 하나로 일괄해서 쓰기위해서 메소드 이름을 mutate라고 정의해보겠습니다.
// id 값도 인자값으로 넘겨주지만 안들어올 수도 있으므로 기본값을 undefined로 한다.
const MsgInput = ({mutate, id = undefined}) => {
// react에서 form 데이터를 다루는 방식이 여러가지가 있겠지만,
// 많은 분들이 onChange 메소드 또는 onInput 메소드들을 쓰시는 걸로 알고 있습니다.
// 그런데 저는 이런 방법 말고 reference 방법을 이용하는 것을 선호합니다.
// 이것은 개인적인 선호도이기 때문에 꼭 이렇게하는 것이 좋다라는 것은 아닙니다.
// 이 방법이 마음에 안드시는 분들은 onChange 또는 onInput 등 으로 하셔도 될거같습니다.
const textRef = useRef(null); // 초기값이 없으면 안됩니다. null값이라도 설정해야됩니다.
const onSubmit = e => {
e.preventDefault();
e.stopPropagation();
const text = textRef.current.value; // text 변수에 textRef.current.value 값을 담습니다.
textRef.current.value = ''; // 그리고 바로 textRef의 값을 지워줍니다.
// mutate 메소드에 id 값도 넘겨준다.
mutate(text, id); // 상위에서 mutate라는 메소드를 받아서 호출할 때 text를 인자값으로 넘겨줍니다. // Create용 메소드가 내려올 수도 있고, Update용 메소드가 내려올수도 있고~
}
return (
<form className='messages__input' onSubmit={onSubmit}>
<textarea
ref={textRef}
placeholder="내용을 입력하세요."
/>
<button type='submit'>완료</button>
</form>
)
}
export default MsgInput
// client/components/MsgItem.js
import MsgInput from "./MsgInput";
const MsgItem = ({
id,
userId,
timestamp,
text,
onUpdate,
isEditing,
startEdit
}) => (
<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} id={id}/>
</>
): text}
<div className="messages__buttons">
<button onClick={startEdit}>수정</button>
</div>
</li>
)
export default MsgItem
이렇게 변경은되는데, 수정창이 안사라진다.
이는 isEditing
값이 계속 true
로 남아있기 때문입니다.
위와 같이 false
로 놓고보면 값이 바뀐게 보이죠?
즉, editing
이 완료되었을 때를 알려주는 메소드가 또 필요할거 같습니다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useState} from "react";
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(originalMsgs);
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();
}
// update가 완료되었다는 것을 알려줍니다.
const doneEdit = () => setEditingId(null)
return (
<>
<MsgInput mutate={onCreate}/>
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id}
{...x}
onUpdate={onUpdate}
startEdit={() => setEditingId(x.id)}
isEditing={editingId === x.id}
/>)
}
</ul>
</>
)
}
export default MsgList;
Delete
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useState} from "react";
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(originalMsgs);
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)
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/MsgItem.js
import MsgInput from "./MsgInput";
const MsgItem = ({
id,
userId,
timestamp,
text,
onUpdate,
onDelete,
isEditing,
startEdit
}) => (
<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} id={id}/>
</>
): text}
<div className="messages__buttons">
<button onClick={startEdit}>수정</button>
<button onClick={onDelete}>삭제</button>
</div>
</li>
)
export default MsgItem
삭제가 잘 됩니다.
위와 같이 MsgList
에서 id
값을 안넘겨주고 MsgItem
에서 위와 같이 id
값을 넘겨줘도됩니다.
한가지 아쉬운점은 위와 같이 수정 버튼을 누르면 기존에 작성되어있던 text가 다 날라가버립니다.
위 아쉬운점 하나만 수정하고 이번 챕터 마무리합시다.
// client/components/MsgInput.js
import {useRef} from "react";
// MsgInput 컴포넌트는 MsgList, MsgItem 컴포넌트에서 사용할 것입니다.
// MsgList에선 Create의 목적으로
// MsgItem에선 Update의 목적으로 사용할 것입니다.
// 때문에 목적이 다르기 때문에 목적이 다른 메소드를 하나로 일괄해서 쓰기위해서 메소드 이름을 mutate라고 정의해보겠습니다.
// id 값도 인자값으로 넘겨주지만 안들어올 수도 있으므로 기본값을 undefined로 한다.
// text 값도 인자값으로 넘겨준다. 하지만 없을수도 있으므로 기본값을 빈문자열로 정의해준다.
const MsgInput = ({mutate, text = '', id = undefined}) => {
// react에서 form 데이터를 다루는 방식이 여러가지가 있겠지만,
// 많은 분들이 onChange 메소드 또는 onInput 메소드들을 쓰시는 걸로 알고 있습니다.
// 그런데 저는 이런 방법 말고 reference 방법을 이용하는 것을 선호합니다.
// 이것은 개인적인 선호도이기 때문에 꼭 이렇게하는 것이 좋다라는 것은 아닙니다.
// 이 방법이 마음에 안드시는 분들은 onChange 또는 onInput 등 으로 하셔도 될거같습니다.
const textRef = useRef(null); // 초기값이 없으면 안됩니다. null값이라도 설정해야됩니다.
const onSubmit = e => {
e.preventDefault();
e.stopPropagation();
const text = textRef.current.value; // text 변수에 textRef.current.value 값을 담습니다.
textRef.current.value = ''; // 그리고 바로 textRef의 값을 지워줍니다.
// mutate 메소드에 id 값도 넘겨준다.
mutate(text, id); // 상위에서 mutate라는 메소드를 받아서 호출할 때 text를 인자값으로 넘겨줍니다. // Create용 메소드가 내려올 수도 있고, Update용 메소드가 내려올수도 있고~
}
return (
<form className='messages__input' onSubmit={onSubmit}>
<textarea
ref={textRef}
defaultValue={text}
placeholder="내용을 입력하세요."
/>
<button type='submit'>완료</button>
</form>
)
}
export default MsgInput
// client/components/MsgItem.js
import MsgInput from "./MsgInput";
const MsgItem = ({
id,
userId,
timestamp,
text,
onUpdate,
onDelete,
isEditing,
startEdit
}) => (
<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}
<div className="messages__buttons">
<button onClick={startEdit}>수정</button>
<button onClick={onDelete}>삭제</button>
</div>
</li>
)
export default MsgItem
수정을 눌렀을 때 현재 textarea
의 내용이 그대로 들어있는 것을 볼 수 있다.
이번 시간에 하나의 완성된 서비스를 아주 빠르게 구현해봤습니다.
물론 프론트엔드단만 했고 서버와의 데이터 통신은 하나도 구현하지 않았고, 오로지 클라이언트에서 임의로 만들어낸 목 데이터를 가지고기본적인 CRUD 기능만 구현을 했습니다.
다음 시간부터 REST API를 하면서 서버쪽 작업을 하면서 JSON DB를 활용해서 서버와 통신하는 것까지 구현해보도록 하겠습니다.