on
MapPin App (맵핀 어플리케이션)
MapPin App (맵핀 어플리케이션)
1. Intro
지도 UI에서 클릭을 통해 장소에 핀을 만들고, 간단한 부연설명과 평점을 넣을 수 있는 어플리케이션이다.
현재 Typescript를 공부하고 있어, 간단한 사이드 프로젝트로 진행했다. (youtuber lamadev의 영상을 참고했다.)
백엔드는 node 서버를 직접 만들어, mongoDB에 로그인, 회원가입, 핀 추가 등의 기능을 만들었다.
프론트부분은 mapbox API와 react-map-gl을 사용했다.
2. Backend
먼저 node 프로젝트 생성 후, 필요한 패키지(express, mongoose, nodemon)을 설치한다.
npm init; npm i express mongoose nodemon;
이후 index.js를 작성했다.
const express = require("express"); const mongoose = require("mongoose"); app.listen(8800, () => { console.log("backend server is running!!"); });
express 서버를 실행할 떄, nodemon을 통해 변화내용을 감지할 수 있도록 할 수 있다.
package.json 파일에서 다음과 같이 스크립트를 수정하면 npm start 커맨드로 로 간단히 실행할 수 있다
"scripts": { "start": "nodemon index.js" },
Server is running
이후 MongoDB와 로컬 노드서버를 연결했다.
MongoDB에서 프로젝트를 만든 후 DB에 접근하려면 ID + PASSWORD가 섞인 URL을 통해 접근이 가능하다.
보안상 이 URL을 .env 파일에 보관하고 따로 git에는 업로드 하지 않기 위해, dotenv란 패키지를 설치해준다.
npm i dotenv
.env에 보관된 url은 process.env를 통해 접근이 가능해진다.
(중요한 것은 dotenv.config()를 통해 .env 파일을 활성해 줘야한다.)
아래는 MongoDB와 연결한 코드이다.
const express = require("express"); const mongoose = require("mongoose"); const dotenv = require("dotenv"); const app = express(); dotenv.config(); // to use body information app.use(express.json()); mongoose .connect(process.env.MONGO_URL, { useNewUrlParser: true, useUnifiedTopology: true, }) .then(() => { console.log("MongoDB connected"); }) .catch((err) => console.log(err)); app.listen(8800, () => { console.log("backend server is running!!"); });
이후 MongoDB에 Pin을 저장하고, 불러오는 api를 설계했다.
먼저 Pin Schema를 만들어야 한다. Schema란 MongoDB에 보낼 데이터 Form 이다.
Pin Schema
const mongoose = require("mongoose"); const PinSchema = new mongoose.Schema( { username: { type: String, require: true }, title: { type: String, require: true, min: 3 }, desc: { type: String, require: true, min: 3 }, rating: { type: Number, required: true, min: 0, max: 5 }, lat: { type: Number, required: true, }, long: { type: Number, required: true, }, }, { timestamps: true } ); // timestamps automaticaly update user createdat, updatedat module.exports = mongoose.model("Pin", PinSchema);
Pin을 작성한 사용자, 핀 이름, 설명, 평점, 위도, 경도, 작성시간의 구조로 되어 있다.
Schma를 활용하여, routes를 활용하면 api 경로를 만들 수 있다.
const router = require("express").Router(); const Pin = require("../models/Pin"); // create a Pin router.post("/", async (req, res) => { const newPin = new Pin(req.body); try { const savedPin = await newPin.save(); // this is async -await res.status(200).json(savedPin); } catch (err) { res.status(500).json(err); } }); // get all Pins router.get("/", async (req, res) => { try { const pins = await Pin.find(); res.status(200).json(pins); } catch (error) { res.status(500).json(err); } }); //export module.exports = router;
API가 제대로 설계 되었는지 확인하려면 Postman App을 사용하면 된다.
(GUI로 쉽게 API에 데이터를 보내고 받는 역할을 수행해주는 앱이다.)
처음에 POST를 할 때, 제대로 동작하지 않았다. 그 이유는 express.json() 이라는 미들웨어를 활성화 하지 않아서 였다.
express.json()은 request의 body를 통해 전달되는 데이터를 JSON Object로 인식하게 해준다.
https://stackoverflow.com/questions/23259168/what-are-express-json-and-express-urlencoded
이후 로그인을 위한 user schema를 만들었다.
const mongoose = require("mongoose"); const UserSchema = new mongoose.Schema( { username: { type: String, require: true, min: 3, max: 20, unique: true }, email: { type: String, require: true, max: 50, unique: true, }, password: { type: String, required: true, min: 6, }, }, { timestamps: true } ); // timestamps automaticaly update user createdat, updatedat module.exports = mongoose.model("User", UserSchema);
아래는 register, login 메서드다. DB에 패스워드를 저장할 떄는, bcrypt 라이브러리를 사용하여 암호화해서 저장했다.
const router = require("express").Router(); const User = require("../models/User"); const bcrypt = require("bcrypt"); //register router.post("/register", async (req, res) => { try { // generate new password const salt = await bcrypt.genSalt(10); const hashedPassword = await bcrypt.hash(req.body.password, salt); //create new user const newUser = new User({ username: req.body.username, email: req.body.email, password: hashedPassword, }); // save user and send respond const user = await newUser.save(); res.status(200).json(user._id); } catch (err) { res.status(500).json(err); } }); //login router.post("/login", async (req, res) => { try { // find user const user = await User.findOne({ username: req.body.username }); !user && res.status(400).json("Wrong username or password"); // validate password const validPassword = await bcrypt.compare( req.body.password, user.password ); !validPassword && res.status(400).json("Wrong username or password!"); //send res res.status(200).json({ _id: user._id, username: user.username }); } catch (err) { res.status(500).json(err); } }); //export module.exports = router;
3. Frontend
react-map-gl 라이브러리를 사용하면 쉽게 mapbox의 지도 UI를 사용가능하다.
이번 프로젝트에서 흥미로웠던 것은 Axios를 사용해서 data를 Get, Post하는 부분이었다.
아래는 useEffect 내에서, axios를 통해 서버에 data를 요청하는 부분이다.
url에 https://cors-anywhere.herokuapp.com/ 부분은 배포하면서 cors 에러가 발생하여, 임시방편으로 사용한 url이다.
이번 프로젝트에서 처음으로 cors 에러를 겪었는데, 이후 포스팅에서 원인과 해결방안을 정리해 볼 계획이다.
useEffect(() => { const getPins = async () => { try { const res = await axios.get( "https://cors-anywhere.herokuapp.com/https://map-pin-project.herokuapp.com/api/pins" ); setPins(res.data); } catch (error) { console.log(error); } }; getPins(); }, []);
아래는 버튼 클릭시, Pin을 포스팅하는 부분이다. 이 역시 Axios를 통해 간단히 구현 가능하다.
const handleSubmit = async (e) => { e.preventDefault(); const newPin = { username: currentUsername, title, desc, rating: star, lat: newPlace.lat, long: newPlace.long, }; try { const res = await axios.post( "https://cors-anywhere.herokuapp.com/https://map-pin-project.herokuapp.com/api/pins", newPin ); setPins([...pins, res.data]); setNewPlace(null); } catch (err) { console.log(err); } };
완성본
from http://developerkhc.tistory.com/63 by ccl(A) rewrite - 2021-11-13 18:00:53