[Node.js] 03. React
마지막에 css를 제공할 예정이니, 각 태그들의 클래스명은 되도록 똑같이 따라하시길 바랍니다.
1. 설치하기
리액트를 만들기 위해 터미널에서 + 버튼을 눌러 새 터미널을 만듭니다.
경로가 MyTodoList에 있는지 잘 확인한 후, 아래 내용을 입력해서 리액트를 설치합니다.
$ npx create-react-app client
cd client
npm start
경로를 클라이언트로 바꾸고 리액트를 실행시켜봅시다.
이 화면이 나온다면 성공입니다.
우선 서버와 클라이언트를 연결해줘야 합니다.
기존의 package.json이 아닌 클라이언트 안에 있는 package.json에 접근해서,
"proxy": "http://localhost:8080"
이 항목을 추가합니다.
2. 파일 수정 및 정리
client>src>app.css에 있는 내용은 다 제거합니다.
logo.svg파일도 필요없으니 삭제합니다.
client>src>App.js에서 함수를 arrow함수 형식으로 변경하고 사용하지 않는 내용은 지워줍니다.
logo도 사용하지 않으니 import시키는 부분도 지워줍니다.
import './App.css';
const App = () => {
return (
<div className="App">
<h2>Todo List</h2>
</div>
);
}
export default App;
3. component만들기
client>src 디렉토리 안에 component 폴더를 생성합니다.
main.jsx파일을 생성합니다.
import React, { useState } from 'react';
export const Main = () => {
const [todo, setTodo] = useState("");
const onChange = (e) => {
setTodo(e.target.value);
}
return (
<form>
<input className="todo_input" type="text" value={todo} name="todo" onChange={onChange} required />
<button type="submit" className="submit_btn">ENTER</button>
</form>
)
}
할일목록을 input으로 받아오겠습니다.
Hook의 useState를 이용해 사용자가 값(value)을 입력할 때마다 변화(onChange)를 감지해 value값을 입력 값으로 변환하도록 했습니다.
이를 확인해보려면 App.js에 Main컴포넌트를 호출해야합니다.
import './App.css';
import {Main} from './component/Main.jsx'
const App = () => {
return (
<div className="App">
<h2>Todo List</h2>
<Main />
</div>
);
}
export default App;
잘생성되었군요.
4. 서버에 데이터 보내기
이제 서버와 통신하여 input에 입력한 값이 content로 넘어가는 작업을 하도록 하겠습니다.
프론트와 백을 통신할 수 있게 도와주는 모듈 axios를 설치합니다.
Main.jsx에 axios를 import를 시킨 후 react를 다시 실행시킵니다.
import axios from 'axios';
<button>에서 submit이 일어날 때마다 form에서 onSubmit이 일어날 것이고,
onSubmit이 일어날 때, 데이터를 전송하는 로직을 작성해봅시다.
import React, { useState } from 'react';
import axios from 'axios';
export const Main = () => {
const [todo, setTodo] = useState("");
const onChange = (e) => {
setTodo(e.target.value);
}
const onSubmit = async (e) => {
e.preventDefault() // 브라우저의 도움없이 직접 이벤트를 처리하겠다
await axios.post('/create', { //REST API에 작성했던 '/create' url 작성
content: todo // content라는 이름에 todo(input의 value)를 담아서 보냄
}).then((res) => { // 전송성공하면
alert('전송성공'); // 알럿을 띄운다.
window.location.reload(); // 화면을 새로고침 한다.
}).catch((err) => { // 에러발생 시,
console.log(err); // 에러 콘솔을 띄움
})
}
return (
<form className="write" onSubmit={onSubmit}>
<input className="todo_input" type="text" value={todo} name="todo" onChange={onChange} required />
<button type="submit" className="submit_btn">ENTER</button>
</form>
)
}
input박스에 내용을 입력한 후, db에서 확인해 봅시다.
잘들어가네요.
5. 데이터 리스트 화면에 불러오기
이제 할일 목록을 화면에 불러와봅시다.
ListManage.jsx 파일을 생성하여 ListManage컴포넌트를 만들어봅시다.
잊지않고 App.js에도 컴포넌트 호출해야합니다.
import './App.css';
import {Main} from './component/Main.jsx'
import {ListManage} from './component/ListManage.jsx'
const App = () => {
return (
<div className="App">
<Main />
<ListManage />
</div>
);
}
export default App;
Hooks의 useEffect는 컴포넌트가 마운트 됐을 때 (처음 나타났을 때), 언마운트 됐을 때 (사라질 때), 그리고 업데이트 될 때 (특정 props가 바뀔 때) 특정 작업을 처리하는 방법입니다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const ListManage = () => {
const [list, setList] = useState([]);
// useEffect는 비동기처리를 할 수 없으므로 비동기처리 함수를 만든 후,
// useEffect에 호출합니다.
const List = async (e) => {
await axios.get('/list')
.then((res) => {
console.log(res.data.result);
// 불러온 데이터를 list에 담습니다.
setList(res.data.result);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
List();
}, [])
return (
<>
// map함수를 이용해서 list에 담았던 데이터를 화면에 뿌려줍니다.
{list.map(k => (
<>
<div className="title">
<p>{k.idx}.</p>
<h3>{k.content}</h3>
</div>
</>
))}
</>
)
}
잘나왔습니다.
5. 수정하기 만들기
수정 버튼을 만들어서 수정을 누르면 해당하는 할일에 input박스를 생성시켜서 수정된 내용을 서버에 전송하는 작업을 해봅시다.
우선 수정버튼을 만들어주어야겠죠.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const ListManage = () => {
const [list, setList] = useState([]);
const List = async (e) => {
await axios.get('/list')
.then((res) => {
console.log(res.data.result);
setList(res.data.result);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
List();
}, [])
return (
<>
{list.map(k => (
<>
<div className="title">
<p>{k.idx}.</p>
<h3>{k.content}</h3>
<button>수정하기</button>
</div>
</>
))}
</>
)
}
수정하기 버튼을 누르면 useState로 content를 받아오는 <p>태그 영역이 <textarea>로 바뀌도록 해봅시다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const ListManage = () => {
const [list, setList] = useState([]);
const [updating, isUpdating] = useState(false);
const List = async (e) => {
await axios.get('/list')
.then((res) => {
console.log(res.data.result);
setList(res.data.result);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
List();
}, [])
return (
<>
{list.map(k => (
<>
{updating === false ?
<div className="title">
<p>{k.idx}.</p>
<h3>{k.content}</h3>
<button onClick={() => isUpdating(true)}>수정하기</button>
</div>
:
<>
<p>{k.idx}.</p>
<input />
<button onClick={() => isUpdating(true)}>수정완료</button>
</>
}
</>
))}
</>
)
}
이렇게 되겠죠.
수정된 데이터를 업데이트하는 코드도 작성해봅시다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const ListManage = () => {
const [list, setList] = useState([]);
const [updating, isUpdating] = useState(false);
const [update, setUpdate] = useState(""); //수정된 값 저장소
const List = async (e) => {
await axios.get('/list')
.then((res) => {
console.log(res.data.result);
setList(res.data.result);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
List();
}, [])
// textarea의 변화를 감지하여 update에 값을 담음
const onChange = (e) => {
setUpdate(e.target.value);
}
// url '/update'로 수정시킬 idx와 update에 담았던 수정할 내용을 content로 보냄
const onUpdate = async (idx) => {
await axios.post('/update', {
idx: idx,
content: update
})
.then((res) => {
console.log('res');
alert('수정이 완료되었습니다.');
window.location.reload();
}).catch((err) => {
console.log(err);
})
}
return (
<>
{list.map(k => (
<>
// 삼항연산자를 이용하여 버튼을 클릭하면 updating이 true가 되면서 textarea를 보여줌
{updating === false ?
<div className="title">
<p>{k.idx}.</p>
<h3>{k.content}</h3>
<button onClick={() => isUpdating(true)}>수정하기</button>
</div>
:
<form onSubmit={() => onUpdate(k.idx)}>
<p>{k.idx}.</p>
<textarea type="text" name="update" onChange={onChange} required>{k.content}</textarea>
<button type="submit">수정완료</button>
</form>
}
</>
))}
</>
)
}
1번 내용을 수정해보면 성공적으로 수정이 됩니다.
그러나 수정하고 싶은 항목만 textarea로 전환시켜야하는데 다른 항목도 같이 되네요.
이를 해결하기 위해선 컴포넌트를 쪼개야합니다.
ListFormat이라는 컴포넌트를 만들어서 작성했던 내용을 붙여넣고,
ListFormat 컴포넌트를 ListManage 컴포넌트에 호출합니다.
각 키값과 함수들을 props를 통해 ListManage 컴포넌트에 전달하도록 합니다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const ListManage = (e) => {
const [list, setList] = useState([]);
const [update, setUpdate] = useState(""); //수정된 값 저장소
const [changeCheck, setChangeCheck] = useState(false);
const List = async (e) => {
await axios.get('/list')
.then((res) => {
console.log(res.data.result);
setList(res.data.result);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
List();
}, [])
const onChange = (e) => {
setChangeCheck(true);
setUpdate(e.target.value);
}
const onUpdate = async (idx) => {
if (changeCheck === true) {
await axios.post('/update', {
idx: idx,
content: update
})
.then((res) => {
console.log('res');
alert('수정이 완료되었습니다.');
window.location.reload();
}).catch((err) => {
console.log(err);
})
}
}
return (
<>
{list.map(k => (
<ListFormat
onUpdate={onUpdate}
onChange={onChange}
idx={k.idx}
content={k.content}
updateContent={k.content}
updateValue={() => setUpdate(e.target.value)}
/>
))}
</>
)
}
export const ListFormat = (props) => {
const [updating, isUpdating] = useState(false);
return (
<div className="list">
{updating === false ?
<>
<div className="title">
<p>{props.idx}.</p>
<h3 onClick={() => isUpdating(true)}>{props.content}</h3>
</div>
</>
:
<form onSubmit={() => props.onUpdate(props.idx)}>
<div className="title">
<p>{props.idx}.</p>
<textarea type="text" name="update" onChange={props.onChange} required>{props.updateContent}</textarea>
</div>
<div className="btn">
<button type="submit">O</button>
<button onClick={() => isUpdating(false)}>X</button>
</div>
</form>
}
</div>
)
}
수정하기 버튼을 누르기보다는 컨텐츠 자체를 클릭하여 내용을 수정하도록 태그 변경했습니다.
수정을 완료할 땐, o버튼을 누르고 취소할 땐 x버튼으로 취소하도록 버튼을 추가했습니다.
그리고 내용을 수정하지 않고 o버튼을 누르면 value에 아무내용도 입력되지 않기 때문에
onChange가 일어나면 chagecheck가 true가 되도록 하고, onupdate함수에서 chagecheck가 true 여야만 수정되는 로직을 작성했습니다.
6. 삭제하기
할일을 완료하면 체크박스를 눌러서 할일을 삭제해봅시다.
삭제 버튼을 추가하고, props로 삭제할 항목의 idx를 넘겨줍니다. input의 id도 idx로 넘겨주면 체크박스의 애니메이션 효과도 해당되는 항목만 일어나니 idx로 넘겨주세요.
onDelete함수로 idx로 구분하여 삭제할 수 있는 로직을 작성하고 setTimeout을 이용하여 애니메이션이 일어나고 난 뒤인 0.5뒤에 화면을 새로고침해봅시다.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
export const ListManage = (e) => {
const [list, setList] = useState([]);
const [update, setUpdate] = useState(""); //수정된 값 저장소
const [changeCheck, setChangeCheck] = useState(false);
const List = async (e) => {
await axios.get('/list')
.then((res) => {
console.log(res.data.result);
setList(res.data.result);
}).catch((err) => {
console.log(err);
})
}
useEffect(() => {
List();
}, [])
const onChange = (e) => {
setChangeCheck(true);
setUpdate(e.target.value);
}
const onUpdate = async (idx) => {
if (changeCheck === true) {
await axios.post('/update', {
idx: idx,
content: update
})
.then((res) => {
console.log('res');
alert('수정이 완료되었습니다.');
window.location.reload();
}).catch((err) => {
console.log(err);
})
}
}
const onDelete = async (idx) => {
await axios.post('/delete', {
idx: idx
})
.then((res) => {
console.log('res');
setTimeout(() => {
window.location.reload();
}, 500);
}).catch((err) => {
console.log(err);
})
}
return (
<>
{list.map(k => (
<ListFormat
onUpdate={onUpdate}
onDelete={onDelete}
onChange={onChange}
idx={k.idx}
content={k.content}
updateContent={k.content}
updateValue={() => setUpdate(e.target.value)}
/>
))}
</>
)
}
export const ListFormat = (props) => {
const [updating, isUpdating] = useState(false);
return (
<div className="list">
{updating === false ?
<>
<div className="title">
<p>{props.idx}.</p>
<h3 onClick={() => isUpdating(true)}>{props.content}</h3>
</div>
<button className="check" onClick={() => props.onDelete(props.idx)}>
<input type="checkbox" id={props.idx} />
<label for={props.idx} class="check-box"></label>
</button>
</>
:
<form onSubmit={() => props.onUpdate(props.idx)}>
<div className="title">
<p>{props.idx}.</p>
<textarea type="text" name="update" onChange={props.onChange} required>{props.updateContent}</textarea>
</div>
<div className="btn">
<button type="submit">O</button>
<button onClick={() => isUpdating(false)}>X</button>
</div>
</form>
}
</div>
)
}
잘지워지는게 보이네요.
서버와 통신하여 CRUD구현 작업이 완료되었습니다~
👏👏👏👏👏짝짝짝짝👏👏👏👏👏👏
잘따라오신 여러분께 css와 scss를 선물로 드리겠습니다.