MERN 스택으로 To-Do 앱을 만드는 단계
필요한 도구
Node와 NPM이 컴퓨터에 설치되어 있는지 확인하십시오. nodejs.org에서 둘 다 다운로드할 수 있습니다(NPM은 Node 설치에 포함됨).
기술 스택
Node.js
Express.js
MongoDB
React.js
Node.js의 종속성
body-parser
mongoose
mongoose-auto-increment
노드(Express) 백엔드 생성
먼저 to-do-node(예:)라는 프로젝트용 폴더를 만듭니다.
그런 다음 코드 편집기에서 해당 폴더를 엽니다.
노드 프로젝트를 만들려면 터미널에서 다음 명령을 실행합니다.
npm init -y
이렇게 하면 모든 앱 스크립트를 추적하고 노드 앱에 필요한 모든 종속성을 관리할 수 있는 package.json 파일이 생성됩니다.
우리는 Express를 사용하여 포트 3000에서 실행되는 간단한 웹 서버를 만들 것입니다.
이제
index.js
라는 이름으로 앱이 실행되기 시작하는 색인 파일을 만들어 보겠습니다.const express = require('express')
const app = express()
const bodyParser = require('body-parser');
//import router
const router = require('./app/index.js');
// body parser
app.use(bodyParser.urlencoded({ limit: '100mb', extended: true }))
app.use(bodyParser.json({ limit: '100mb', extended: true }))
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
if ('OPTIONS' === req.method) {
//respond with 200
res.send(200);
}
else {
//move on
next();
}
});
app.get('/', (req, res) => {
res.send("incorrect route");
})
//add routes
const base = '/api/v1/';
app.use(base, router);
app.listen(process.env.PORT || 3000, () => console.log('Running on port 3000!'));
그런 다음 터미널에서 필요한 종속성을 설치합니다.
npm I express mongoose body-parser nodemon mongoose-auto-increment
프로젝트 폴더 안에
app
라는 폴더를 만들고 index.js
파일을 추가하여 경로를 정의합니다.const express = require('express');
// Routes Import
const toDo = require("./toDo/index.js");
const router = express.Router();
// Adding Routes
router.use('/to-do', toDo);
module.exports = router;
config라는 폴더 이름을 만들고 거기에
db.js
파일을 추가합니다.db.js
파일의 구성은 다음과 같습니다.const mongoose = require('mongoose');
mongoose.connect('mongodb+srv://<mongodb_username>:<cluster_password>@cluster0.nxcni.mongodb.net/toDo?retryWrites=true&w=majority', { useNewUrlParser: true });
module.exports = mongoose.connection;
이제 API를 작성해 봅시다.
toDo라는 폴더를 만들고 그 안에
model.js
, index.js
및 router.js
파일을 만듭니다.index.js
module.exports = require("./router");
model.js
const mongoose = require('mongoose');
const autoIncrement = require('mongoose-auto-increment');
const db = require('../config/db.js');
autoIncrement.initialize(db);
const schema = new config.mongoose.Schema({
toDos: [
{
toDo: String,
tag: String,
tagColor: String,
done: Boolean
}
],
createdAt: {
type: Date,
default: Date.now
},
updatedAt: {
type: Date,
default: Date.now
},
status: {
type: Boolean,
default: true
}
}, {
strict: true
});
var toDo = mongoose.model('toDos', schema);
module.exports = toDo;
라우터.js
const config = require('../config/routes.js');
const router = config.express.Router();
const collection = require('./model.js');
// @route GET api/v1/to-do/list
// @desc get users list with pagination
// @access Public
router.get('/list', function (req, res) {
if(!req.query.id) {
res.status(200).send({data: []});
return false;
}
getToDosList(req.query.tag ? req.query.tag : '', req.query.id).then(resp => {
res.status(200).send(resp[0]);
}, err => {
res.status(500).send({message: "Something went wrong, please try after sometime"});
})
});
// @route CREATE api/v1/to-do/add
// @desc add to-do
// @access Public
router.post('/add', function(req, res) {
if(!req.query.id) {
collection.create({toDos: [{toDo: req.body.text, done: false, tag: req.body.tag, tagColor: req.body.tagColor}]}, function (err, toDo) {
if (!err) {
return res.status(200).json({error: false, data: toDo, message: 'success'})
} else {
return res.status(500).send({error: true, message: 'Error adding to-do'})
}
});
} else {
let updateData = {
$push: {
"toDos": {toDo: req.body.text, done: false, tag: req.body.tag, tagColor: req.body.tagColor}
}
};
updateToDo({_id: req.query.id}, updateData).then(toDo => {
return res.status(200).json({error: false, data: toDo, message: 'success'})
}, err => {
return res.status(500).send({error: true, message: 'Error adding to-do'})
})
}
});
// @route UPDATE api/v1/to-do/done
// @desc update toDo status
// @access Public
router.put('/done/:userId/:toDoId', function(req, res) {
let updateData = {
$set: {
"toDos.$.done": req.body.done
}
};
updateToDo({_id: req.params.userId, "toDos._id": req.params.toDoId}, updateData).then((toDo) => {
return res.status(200).json({error: false, message: 'Updated successfully'})
}, err => {
return res.status(500).send({error: true, message: err})
})
});
// @route UPDATE api/v1/to-do/delete
// @desc delete toDo
// @access Public
router.put('/delete/:userId/:toDoId', function(req, res) {
let updateData = { "$pull": { "toDos": { "_id": req.params.toDoId } } }
updateToDo({_id: req.params.userId, "toDos._id": req.params.toDoId}, updateData).then((toDo) => {
return res.status(200).json({error: false, message: 'Updated successfully'})
}, err => {
return res.status(500).send({error: true, message: err})
})
});
// function to get to-dos list with tag filter
function getToDosList(tag, id) {
return new Promise(function(resolve, reject) {
let agg = [
{
"$unwind": "$toDos"
}, {
"$match": {
$or: [{"_id": id}, {"toDos.tag": {$regex: `${tag}.*`, $options: "i" }}]
}
}, {
"$group": {
_id: null,
data: {$push: "$toDos"}
}
}
]
collection.aggregate(agg, function(err, response) {
if(err) return reject({message: "Something went wrong"})
if(!response) return reject({message: "Error while getting remitters data"})
return resolve(response)
})
})
}
//function to update to-do
function updateToDo(query, updateData) {
return new Promise(function(resolve, reject) {
collection.findOneAndUpdate(query, updateData, {new: true},
function (err, resp) {
if (err) return reject({error: 1, message: "There was a problem while updating data"});
return resolve(resp);
}
);
})
}
function getToDos(query) {
return new Promise(function(resolve, reject) {
collection.find(query,
function (err, resp) {
if (err) return reject({error: 1, message: "There was a problem while updating data"});
return resolve(resp);
}
);
})
}
module.exports = router
마지막으로 터미널에서
nodemon index.js
를 실행하여 앱을 실행할 수 있으며 앱이 포트 3000에서 실행되고 있음을 확인할 수 있습니다.React.js의 종속성
bootstrap
react-bootstrap
react-icons
React 프런트엔드 만들기
백엔드를 생성한 후 프런트엔드로 이동해 보겠습니다.
다른 터미널 탭을 열고 create-react-app을 사용하여 to-do-react라는 이름으로 새 React 프로젝트를 만듭니다(예:).
npx create-react-app to-do-react
그런 다음 모든 종속성이 설치된 React 앱을 갖게 됩니다.
이제 폴더로 이동
cd to-do-react
디렉토리를 src로 변경하고 아래 명령을 실행합니다.
cd src
rm *
이제 아래 명령을 실행하여 index.js 파일을 만듭니다.
touch index.js
이 파일은 public 폴더에 있는 HTML 파일로 앱을 렌더링합니다. 또한 파일 이름
components
으로 폴더 이름app.js
을 만듭니다.mkdir components && cd components && touch app.js
app.js에는 To-Do 앱이 포함됩니다.
src에서 파일 편집
index.js
:import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import 'bootstrap/dist/css/bootstrap.min.css';
ReactDOM.render(<App/>, document.getElementById('root'));
이름이
api
인 폴더를 만들고 이름이 to-do.js
인 파일을 추가하고 해당 파일에 아래와 같이 API 호출을 작성합니다.import axios from 'axios';
let base = 'http://localhost:3000/api/v1/';
export default function api(url, method='GET', data={}) {
return new Promise(function(resolve, reject) {
const requestOptions = {
url: base + url,
method: method,
headers: {
'Content-Type': 'application/json'
},
data
};
axios(requestOptions)
.then(function (response) {
resolve(response.data);
})
.catch(function (error) {
reject(error);
});
});
}
export function AddToDoAPI(data) {
return new Promise(function(resolve, reject) {
api(`to-do/add?id=${localStorage.userId ? localStorage.userId : ''}`, 'POST', data)
.then((resp) => {
return resolve(resp);
}, (error) => {
return reject(error.response.data.message);
})
})
}
export function GetToDoListAPI(tag='') {
return new Promise(function(resolve, reject) {
api(`to-do/list?id=${localStorage.userId ? localStorage.userId : ''}&tag=${tag}`)
.then((resp) => {
return resolve(resp);
}, (error) => {
console.log(error)
debugger
return reject(error.response.data.message);
})
})
}
export function UpdateToDoAPI(data, toDoId) {
return new Promise(function(resolve, reject) {
api(`to-do/done/${localStorage.userId}/${toDoId}`, 'PUT', data)
.then((resp) => {
return resolve(resp);
}, (error) => {
return reject(error.response.data.message);
})
})
}
export function DeleteToDoAPI(toDoId) {
return new Promise(function(resolve, reject) {
api(`to-do/delete/${localStorage.userId}/${toDoId}`, 'PUT', {})
.then((resp) => {
return resolve(resp);
}, (error) => {
return reject(error.response.data.message);
})
})
}
구성 요소에서 편집
app.js
:import React, {Component} from 'react';
// Bootstrap for react
import Container from 'react-bootstrap/Container';
import Row from 'react-bootstrap/Row';
import Col from 'react-bootstrap/Col';
import Button from 'react-bootstrap/Button';
import InputGroup from 'react-bootstrap/InputGroup';
import FormControl from 'react-bootstrap/FormControl';
import ListGroup from 'react-bootstrap/ListGroup';
import Form from 'react-bootstrap/Form'
import Dropdown from 'react-bootstrap/Dropdown'
import DropdownButton from 'react-bootstrap/DropdownButton'
import {AddToDoAPI, GetToDoListAPI, UpdateToDoAPI, DeleteToDoAPI} from '../api/to-do'
import { BsStop, BsX } from 'react-icons/bs';
import {Badge} from "react-bootstrap";
class AppComponent extends Component {
constructor(props) {
super(props);
// Setting up state
this.state = {
userInput : "",
list:[],
selectedTag: "Other",
selectedTagColor: "grey",
tags: [
{tagName: 'Other', color: 'grey'},
{tagName: 'Work', color: 'red'},
{tagName: 'Personal', color: 'green'}
]
}
}
componentDidMount() {
this.getItems()
}
// Set a user input value
updateInput(value){
this.setState({
userInput: value,
});
}
// Set a selected tag value
updateTag(value){
this.setState({
selectedTag: value.split(" ")[0],
selectedTagColor: value.split(" ")[1]
});
}
// Add item if user input in not empty
addItem(event){
if(event.code === 'Enter') {
AddToDoAPI({text: this.state.userInput, tag: this.state.selectedTag, tagColor: this.state.selectedTagColor}).then(resp => {
if(!localStorage.userId) {
localStorage.setItem('userId', resp.data._id);
}
this.getItems()
})
}
}
//Get to-do list
getItems(tag='') {
GetToDoListAPI(tag).then(resp => {
// Update list
const list = [...resp ? resp.data : []];
// reset state
this.setState({
list,
userInput: ""
});
})
}
UpdateToDo(val, id) {
UpdateToDoAPI({done: val}, id).then(resp => {
this.getItems()
})
}
// Function to delete item from list use id to delete
deleteItem(id) {
DeleteToDoAPI(id).then(resp => {
this.getItems()
})
}
render(){
return(
<Container>
<Row style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
fontSize: '3rem',
fontWeight: 'bolder',
fontFamily: 'DejaVu Sans Mono, monospace',
paddingTop: 2
}}
>TODO LIST
</Row>
<hr style={{marginTop: 0}}/>
<Row>
<Col md={{ span: 5, offset: 4 }}>
<InputGroup className="mb-3">
<DropdownButton
variant="outline-secondary"
id="input-group-dropdown-2"
title={this.state.selectedTag}
align="end"
size="lg"
style={{backgroundColor: 'white'}}
onSelect = {e => this.updateTag(e)}
>
{this.state.tags.map(tag => (
<span style={{display: 'flex'}}><BsStop style={{fontSize: 30, marginTop: 1, color: tag.color}}/><Dropdown.Item key={tag.tagName} eventKey={tag.tagName + ' ' + tag.color}>{tag.tagName}</Dropdown.Item></span>
))}
</DropdownButton>
<FormControl
placeholder="add item . . . "
size="lg"
value = {this.state.userInput}
onChange = {item => this.updateInput(item.target.value)}
onKeyPress = {e => this.addItem(e)}
aria-label="add something"
aria-describedby="basic-addon2"
/>
</InputGroup>
</Col>
</Row>
{this.state.list.length ?
<Row>
<Col md={{ span: 5, offset: 4 }} style={{paddingBottom: 18}}>
<Button variant="primary" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('')} size="sm">All</Button>{' '}
<Button variant="secondary" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Other')} size="sm">Other</Button>{' '}
<Button variant="danger" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Work')} size="sm">Work</Button>{' '}
<Button variant="success" style={{paddingTop: 0, paddingBottom: 0}} onClick={e => this.getItems('Personal')} size="sm">Personal</Button>{' '}
</Col>
</Row> : null
}
<Row>
<Col md={{ span: 5, offset: 4 }}>
<ListGroup>
{/* map over and print items */}
{this.state.list.map(item => {return(
<ListGroup.Item variant="white" action
key={item._id}>
<Form.Group id="formGridCheckbox" style={{display: 'flex'}}>
<Form.Check type="checkbox" style={{width: 10}} className="my-1 mr-sm-2" onChange={e => this.UpdateToDo(!item.done, item._id)} checked={item.done}/>
{item.done ? <span style={{textDecoration: 'line-through', marginTop: 5, width: 380}}>{item.toDo}</span> : <span style={{marginTop: 5, width: 380}}>{item.toDo}</span>}
<BsStop style={{fontSize: 25, marginTop: 1, color: item.tagColor, float: 'right', width: 30}}/>
<BsX
onClick = { () => this.deleteItem(item._id) }
style={{float: 'right', fontSize: 25, marginLeft: 'auto', width: 30}}
/>
</Form.Group>
</ListGroup.Item>
)})}
</ListGroup>
</Col>
</Row>
</Container>
);
}
}
export default AppComponent;
터미널에 다음 명령을 입력하여 서버를 시작합니다.
npm start
출력: 브라우저에서 열기
http://localhost:3000
:또한 필터를 적용하고 개인화된 할 일을 확인하세요.
Reference
이 문제에 관하여(MERN 스택으로 To-Do 앱을 만드는 단계), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/chytrakr/steps-to-create-a-to-do-app-with-mern-stack-4912텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)