그런데 MERN 스택인... 풀스택 가능한..
도전~~
mern 디렉터리: 전체 프로젝트 파일 모아두는 곳
mern 디렉터리 내부에 리액트 클라이언트 앱 디렉터리를 생성한다.
mkdir mern
cd mern
npx create-react-app client
리액트 앱 생성 후 백엔드를 위한 폴더를 생성하고 server라고 이름 설정
mkdir server
cd server
npm init -y
package.json 초기화 후에 의존성 설치해주기
npm install mongodb express cors dotenv
mongodb
MongoDB 데이터베이스 드라이버 설치. (Node.js 애플리케이션과 데이터 베이스를 연결하기 위해 드라이버가 필요하다.
express
Node.js를 위한 웹 프레임워크 설치
cors
cross origin resource sharing을 허용하기 위한 모듈
dotenv
.env 파일에서 환경변수를 로드하는 모듈을 설치
모듈 설치된 것 확인 후에 server.js파일 생성하기
// 설치한 express module을 불러와서 변수(express)에 저장
const express = require("express");
//e xpress를 실행하여 app object를 초기화
const app = express();
const cors = require("cors");
require("dotenv").config({ path: "./config.env" });
// 사용할 포트 번호를 port 변수에
const port = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
app.use(require("./routes/record"));
// get driver connection
const conn = require("./db/conn");
// port변수를 이용하여 포트에 node.js 서버를 연결
app.listen(port, () => {
// 서버 시작시 DB 연결 수행
conn.connectToServer(function (err) {
if (err) console.log(err);
});
console.log(`Server is now running on port : ${port}`);
});
MongoDB Atlas를 데이터베이스로 사용한다. 사용하기 위해서 계정을 생성하고 클러스터를 배포하고 클러스터의 connection string이 필요하다.
server 디렉터리 아래에 config.env
파일 생성
ATLAS_URI=mongodb+srv://jueon-admin:<password>@cluster0.g2eem.mongodb.net/?retryWrites=true&w=majority
db 폴더 생성 후 내부에 conn.js 파일 생성
const { MongoClient } = require("mongodb");
const db = process.env.ATLAS_URI;
const client = new MongoClient(db, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
var _db;
module.exports = {
connectToServer: function (callback) {
client.connect(function (err, db) {
if (db) {
_db = db.db("employees");
console.log("Successfully connected to MongoDB.");
}
return callback(err);
});
},
getDB: function () {
return _db;
},
};
_db = db.db("employees")
@param dbName
: 사용하고자하는 데이터베이스의 이름데이터베이스 설정 완료했고, 서버 설정도 완료했다
routes 폴더 생성하고 내부에 record.js 파일을 생성한다.
const express = require("express");
// recordRoutes : express router 인스턴스
// 루트 정의를 위해 사용한다.
// 라우터는 미들웨어로 추가될 것이고 /record 로 시작하는 요청에 대한 제어를 수행
const recordRoutes = express.Router();
// DB와 연결을 위해서 사용한다.
const dbconn = require("../db/conn");
// 문자열 id를 객체 id로 변환한다. (id -> _id)
const ObjectId = require("mongodb").ObjectId;
// 레코드 리스트 받기
recordRoutes.route("/record").get(function (req, res) {
let db_connect = dbconn.getDB("employees");
db_connect
.collection("records")
.find({})
.toArray(function (err, result) {
if (err) throw err;
res.json(result);
});
});
// 개별 레코드 받기
recordRoutes.route("/record/:id").get(function (req, res) {
let db_connect = dbconn.getDB();
let query = { _id: ObjectId(req.params.id) };
db_connect.collection("records").findOne(query, function (err, result) {
if (err) throw err;
res.json(result);
});
});
// 레코드 생성
recordRoutes.route("/record/add").post((req, res) => {
let db_connect = dbconn.getDB();
let item = {
name: req.body.name,
position: req.body.position,
level: req.body.level,
};
db_connect.collection("records").insertOne(item, (err, result) => {
if (err) throw err;
res.json(result);
});
});
// 레코드 수정
recordRoutes.route("/update/:id").post((req, res) => {
let db_connect = dbconn.getDB();
let query = { _id: ObjectId(req.params.id) };
let newValues = {
$set: {
name: req.body.name,
position: req.body.position,
level: req.body.level,
},
};
db_connect
.collection("records")
.updateOne(query, newValues, (err, result) => {
if (err) throw err;
console.log("1 document updated");
res.json(result);
});
});
recordRoutes.route("/:id").delete((req, res) => {
let db_connect = dbconn.getDB();
let query = { _id: ObjectId(req.params.id) };
db_connect.collection("records").deleteOne(myquery, function (err, obj) {
if (err) throw err;
console.log("1 document deleted");
response.json(obj);
});
});
module.exports = recordRoutes;
모듈 설치
npm install bootstrap react-router-dom
create-react-app 명령어로 생성된 리액프 프로젝트 내부의 src 폴더 안의 모든 파일을 지우고 index.js, App.js 파일을 생성한다.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
document.getElementById("root")
);
그냥 리액트 ...
컴포넌트를 생성해주면 된다..
fetch
를 이용하여 리액트 앱과 백엔드 서버를 연결한다. Create.jsx, Edit.jsx, RecordList.jsx는 HTTP Request를 다루기 때문에 fetch함수를 사용하고 있다.
import React, { useState } from "react";
import { useNavigate } from "react-router-dom";
const Create = () => {
const [form, setForm] = useState({
name: "",
position: "",
level: 1,
});
const navigate = useNavigate();
function updateForm(value) {
return setForm((prev) => {
return { ...prev, ...value };
});
}
async function onSubmit(e) {
e.preventDefault();
const newPerson = { ...form };
await fetch("http://localhost:5050/record/add", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(newPerson),
}).catch((err) => {
window.alert(err);
return;
});
setForm({ name: "", position: "", level: 1 });
navigate("/");
}
return (
<div>
<h3>CREATE NEW RECORD</h3>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
className="form-control"
id="name"
value={form.name}
onChange={(e) => updateForm({ name: e.target.value })}
/>
</div>
<div className="form-group">
<label htmlFor="position">Position</label>
<input
type="text"
className="form-control"
id="position"
value={form.position}
onChange={(e) => updateForm({ position: e.target.value })}
/>
</div>
<div className="form-group">
<div className="form-check form-check-inline">
<input
type="radio"
className="form-check-input"
id="positionIntern"
value="Intern"
checked={form.level === 1}
onChange={(e) => {
updateForm({ level: e.target.value });
}}
/>
<label htmlFor="positionIntern" className="form-check-label">
Intern
</label>
</div>
</div>
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="positionOptions"
id="positionJunior"
value="Junior"
checked={form.level === 2}
onChange={(e) => updateForm({ level: e.target.value })}
/>
<label htmlFor="positionJunior" className="form-check-label">
Junior
</label>
</div>
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="positionOptions"
id="positionSenior"
value="Senior"
checked={form.level === "Senior"}
onChange={(e) => updateForm({ level: e.target.value })}
/>
<label htmlFor="positionSenior" className="form-check-label">
Senior
</label>
</div>
<div className="form-group">
<input
type="submit"
value="Create person"
className="btn btn-primary"
/>
</div>
</form>
</div>
);
};
export default Create;
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom";
const Edit = () => {
const [form, setForm] = useState({
name: "",
position: "",
level: 1,
records: [],
});
const params = useParams();
const navigate = useNavigate();
useEffect(() => {
async function fetchData() {
const id = params.id.toString();
const response = await fetch(
`http://localhost:5050/record/${params.id.toString()}`
);
if (!response.ok) {
const message = `An error has occurred: ${response.statusText}`;
window.alert(message);
return;
}
const record = await response.json();
if (!record) {
window.alert(`Record with id ${id} not found`);
navigate("/");
return;
}
setForm(record);
}
fetchData();
return;
}, [params.id, navigate]);
function updateForm(value) {
return setForm((prev) => {
return { ...prev, ...value };
});
}
async function onSubmit(e) {
e.preventDefault();
const editedPerson = {
name: form.name,
position: form.position,
level: form.level,
};
// POST 요청 전송
await fetch(`http://localhost:5000/update/${params.id}`, {
method: "POST",
body: JSON.stringify(editedPerson),
headers: {
"Content-Type": "application/json",
},
});
navigate("/");
}
return (
<div>
<h3>Update Record</h3>
<form onSubmit={onSubmit}>
<div className="form-group">
<label htmlFor="name">Name: </label>
<input
type="text"
className="form-control"
id="name"
value={form.name}
onChange={(e) => updateForm({ name: e.target.value })}
/>
</div>
<div className="form-group">
<label htmlFor="position">Position: </label>
<input
type="text"
className="form-control"
id="position"
value={form.position}
onChange={(e) => updateForm({ position: e.target.value })}
/>
</div>
<div className="form-group">
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="positionOptions"
id="positionIntern"
value="Intern"
checked={form.level === 1}
onChange={(e) => updateForm({ level: e.target.value })}
/>
<label htmlFor="positionIntern" className="form-check-label">
Intern
</label>
</div>
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="positionOptions"
id="positionJunior"
value="Junior"
checked={form.level === 2}
onChange={(e) => updateForm({ level: e.target.value })}
/>
<label htmlFor="positionJunior" className="form-check-label">
Junior
</label>
</div>
<div className="form-check form-check-inline">
<input
className="form-check-input"
type="radio"
name="positionOptions"
id="positionSenior"
value="Senior"
checked={form.level === 3}
onChange={(e) => updateForm({ level: e.target.value })}
/>
<label htmlFor="positionSenior" className="form-check-label">
Senior
</label>
</div>
</div>
<br />
<div className="form-group">
<input
type="submit"
value="Update Record"
className="btn btn-primary"
/>
</div>
</form>
</div>
);
};
export default Edit;
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
const Record = (props) => (
<tr>
<td>{props.record.name}</td>
<td>{props.record.position}</td>
<td>{props.record.level}</td>
<td>
<Link className="btn btn-link" to={`/edit/${props.record._id}`}>
Edit
</Link>{" "}
|
<button
className="btn btn-link"
onClick={() => {
props.deleteRecord(props.record._id);
}}
>
Delete
</button>
</td>
</tr>
);
const RecordList = () => {
const [records, setRecords] = useState([]);
// DB에서 데이터 가져오기
useEffect(() => {
async function getRecords() {
const response = await fetch(`http://localhost:5050/record/`);
if (!response.ok) {
const message = `An error occurred: ${response.statusText}`;
window.alert(message);
return;
}
const records = await response.json();
setRecords(records);
}
getRecords();
return;
}, [records.length]);
async function deleteRecord(id) {
await fetch(`http://localhost:5050/${id}`, {
method: "DELETE",
});
const newRecords = records.filter((el) => el._id !== id);
setRecords(newRecords);
}
function recordList() {
return records.map((record) => {
return (
<Record
record={record}
deleteRecord={() => deleteRecord(record._id)}
key={record._id}
/>
);
});
}
return (
<div>
<h3>Record List</h3>
<table className="table table-striped" style={{ marginTop: 20 }}>
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Level</th>
<th>Action</th>
</tr>
</thead>
<tbody>{recordList()}</tbody>
</table>
</div>
);
};
export default RecordList;
import React, { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import { useNavigate } from "react-router-dom";
const Record = (props) => (
<tr>
<td>{props.record.name}</td>
<td>{props.record.position}</td>
<td>{props.record.level}</td>
<td>
<Link className="btn btn-link" to={`/edit/${props.record._id}`}>
Edit
</Link>{" "}
|
<button
className="btn btn-link"
onClick={() => {
props.deleteRecord(props.record._id);
}}
>
Delete
</button>
</td>
</tr>
);
const RecordList = () => {
const [records, setRecords] = useState([]);
// DB에서 데이터 가ㅈㅕ오기
useEffect(() => {
async function getRecords() {
const response = await fetch(`http://localhost:5050/record/`);
if (!response.ok) {
const message = `An error occurred: ${response.statusText}`;
window.alert(message);
return;
}
const records = await response.json();
setRecords(records);
}
getRecords();
return;
}, [records.length]);
async function deleteRecord(id) {
await fetch(`http://localhost:5050/${id}`, {
method: "DELETE",
});
const newRecords = records.filter((el) => el._id !== id);
setRecords(newRecords);
}
function recordList() {
return records.map((record) => {
return (
<Record
record={record}
deleteRecord={() => deleteRecord(record._id)}
key={record._id}
/>
);
});
}
return (
<div>
<h3>Record List</h3>
<table className="table table-striped" style={{ marginTop: 20 }}>
<thead>
<tr>
<th>Name</th>
<th>Position</th>
<th>Level</th>
<th>Action</th>
</tr>
</thead>
<tbody>{recordList()}</tbody>
</table>
</div>
);
};
export default RecordList;
import React from "react";
import { Route, Routes } from "react-router-dom";
import Create from "./components/Create";
import Edit from "./components/Edit";
import Navbar from "./components/Navbar";
import RecordList from "./components/RecordList";
const App = () => {
return (
<div>
<Navbar />
<Routes>
<Route exact path="/" element={<RecordList />} />
<Route path="/edit/:id" element={<Edit />} />
<Route path="/create" element={<Create />} />
</Routes>
</div>
);
};
export default App;