Zero To One
MVC 패턴 본문
1. MVC 패턴이란?
- 모델 (Model), 뷰(View), 컨트롤러 (Controller)로 이루어진 디자인 패턴
- 애플리케이션의 구성 요소를 3가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발 가능
- 장점 : 재사용성, 확장성
- 단점 : 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해짐
2. 구성요소
2-1. 모델
- 데이터와 비즈니스 로직을 관리
- 앱이 포함해야할 데이터가 무엇인지를 정의한다. (데이터베이스, 상수, 변수 등)
- 뷰 (View)에서 데이터를 생성하거나 수정하면 컨트롤러를 통해 모델을 생성하거나 갱신한다
2-2. 뷰
- 레이아웃과 화면을 처리
- 뷰는 항목이 사용자에게 보여지는 방식을 정의하며, 표시할 데이터를 모델로부터 받는다
- 모델이 가지고 있는 정보를 따로 저장하지 않아야 하며, 화면에 표시하는 정보만 가지고 있어야 한다
2-3. 컨트롤러
- 명령을 모델과 뷰 부분으로 라우팅
- 앱의 사용자로부터의 입력에 대한 응답으로 모델 및/또는 뷰를 업데이트하는 로직을 포함한다.
- 예를 들어, 쇼핑 리스트는 항목을 추가하거나 제거할 수 있게 해주는 입력 폼과 버튼을 갖는다.
이러한 액션들은 모델이 업데이트되는 것이므로 입력이 컨트롤러에게 전송되고, 모델을 적당하게 처리한다음, 업데이트된 데이터
를 뷰로 전송한다
참조https://developer.mozilla.org/ko/docs/Glossary/MVC
MVC - 용어 사전 | MDN
MVC (모델-뷰-컨트롤러) 는 사용자 인터페이스, 데이터 및 논리 제어를 구현하는데 널리 사용되는 소프트웨어 디자인 패턴입니다. 소프트웨어의 비즈니스 로직과 화면을 구분하는데 중점을 두고
developer.mozilla.org
3. 현재 backend 코드
//app.js
import express from 'express';
import cors from 'cors';
import morgan from 'morgan'; //디버깅용
import helmet from 'helmet'; //보안용
import 'express-async-errors'; //promise나 async error를 잡는용
import tweetsRouter from './router/tweets.js'
const app = express();
app.use(express.json());
app.use(helmet());
app.use(cors());
app.use(morgan('tiny'));
app.use('/tweets', tweetsRouter); // "/tweets"로 접속했을 때 tweetsRoute로 연결
app.use((req, res, next) => { //다른 url이 왔을때 404로 처리
res.sendStatus(404);
});
app.use((error, req, res, next) => { //서버 error 처리
console.error(error);
res.sendStatus(500);
});
app.listen(8080);
// router/tweets.js
import express from 'express';
import 'express-async-errors';
let tweets = [{
id:'1',
text: "test용 텍스트",
createdAt: Date.now().toString(),
name: 'jaehyeok',
username: 'vnfmsqkek3',
url: 'https://www.linkedin.com/in/%EC%9E%AC%ED%98%81-%EC%B5%9C-515606241/'
},
{
id:'2',
text: "test용 텍스트2",
createdAt: Date.now().toString(),
name: 'Zero',
username: 'ttt',
},
];
const router = express.Router();
// GET /tweets
// GET /tweets?username=:username
router.get('/', (req, res, next) => {
const username = req.query.username;
//username이 있다면 현재 가지고 있는 tweets에서 filter를 해준다.
//가지고 있는 배열의 아이템이 트윗을 전달받아서 트윗에 있는 username이 사용자가 원하는 username과 동일한 것만 골라낸다
//username이 없는 경우라면 tweets를 할당한다
const data = username
? tweets.filter(tweet => tweet.username === username)
: tweets;
res.status(200).json(data);
});
// GET /tweets/:id
router.get('/:id', (req, res, next) => {
const id = req.params.id;
const tweet = tweets.find(tweet => tweet.id === id);
if(tweet) {
res.status(200).json(tweet);
}
else {
res.status(404).json({message: "Tweet ${id} not found"});
}
});
// POST /tweets
router.post('/', (req, res, next) => {
const { text, name, username } = req.body;
const tweet = {
id: Date.now().toString(), //mysql id를 대체
text,
createdAt: new Date(),
name,
username,
};
tweets = [tweet, ...tweets];
res.status(201).json(tweet);
});
// PUT /tweets/:id
router.put('/:id', (req, res, next) => {
const id = req.params.id;
const text = req.body.text;
const tweet = tweets.find((tweet) => tweet.id === id);
if (tweet){
tweet.text = text;
res.status(200).json(tweet);
}
else {
res.status(404).json({message: "Tweet $({id}) not found"});
}
});
// DELETE /tweets/:id
router.delete('/:id', (req, res, next) => {
const id = req.params.id;
tweets = tweets.filter((tweet) => tweet.id !== id);
res.sendStatus(204);
})
export default router;
4. 문제상황
- 현재 router안에 데이터와 처리 로직들이 함께 존재
- 수십~수백개의 엔드포인트가 존재할 경우, 유지 관리하기 쉽지 않음
5. 개선 (진행중)
5-1. data 분리
// router/tweets.js
import express from 'express';
import 'express-async-errors';
import * as tweetRepository from '../data/tweet.js';
const router = express.Router();
// GET /tweets
// GET /tweets?username=:username
router.get('/', (req, res, next) => {
const username = req.query.username;
//username이 있다면 현재 가지고 있는 tweets에서 filter를 해준다.
//가지고 있는 배열의 아이템이 트윗을 전달받아서 트윗에 있는 username이 사용자가 원하는 username과 동일한 것만 골라낸다
//username이 없는 경우라면 tweets를 할당한다
const data = username
? tweetRepository.getAllByUsername(username)
: tweetRepository.getAll();
res.status(200).json(data);
});
// GET /tweets/:id
router.get('/:id', (req, res, next) => {
const id = req.params.id;
const tweet = tweetRepository.getById(id);
if(tweet) {
res.status(200).json(tweet);
}
else {
res.status(404).json({message: "Tweet id(${id}) not found"});
}
});
// POST /tweets
router.post('/', (req, res, next) => {
const { text, name, username } = req.body;
const tweet = tweetRepository.creat(text, name, username);
res.status(201).json(tweet);
});
// PUT /tweets/:id
router.put('/:id', (req, res, next) => {
const id = req.params.id;
const text = req.body.text;
const tweet = tweetRepository.update(id, text);
if (tweet){
res.status(200).json(tweet);
}
else {
res.status(404).json({message: "Tweet id(${id}) not found"});
}
});
// DELETE /tweets/:id
router.delete('/:id', (req, res, next) => {
const id = req.params.id;
tweetRepository.remove(id);
res.sendStatus(204);
})
export default router;
// data/tweet.js
let tweets = [{
id:'1',
text: "test용 텍스트",
createdAt: Date.now().toString(),
name: 'jaehyeok',
username: 'vnfmsqkek3',
url: 'https://www.linkedin.com/in/%EC%9E%AC%ED%98%81-%EC%B5%9C-515606241/'
},
{
id:'2',
text: "test용 텍스트2",
createdAt: Date.now().toString(),
name: 'Zero',
username: 'ttt',
},
];
export function getAll(){
return tweets;
}
export function getAllByUsername(username){
return tweets.filter((tweet) => tweet.username === username);
}
export function getById(id){
return tweets.find((tweet) => tweet.id === id);
}
export function creat(text, name, username) {
const tweet = {
id: Date.now().toString(), //mysql id를 대체
text,
createdAt: new Date(),
name,
username,
};
tweets = [tweet, ...tweets];
return tweet;
}
export function update(id, text) {
const tweet = tweets.find((tweet) => tweet.id === id);
if (tweet){
tweet.text = text;
}
return tweet;
}
export function remove(id) {
tweets = tweets.filter((tweet) => tweet.id !== id);
}
5-2. 컨트롤러 분리
// controller/tweets.js
import * as tweetRepository from '../data/tweet.js';
export async function getTweets(req, res) {
const username = req.query.username;
//username이 있다면 현재 가지고 있는 tweets에서 filter를 해준다.
//가지고 있는 배열의 아이템이 트윗을 전달받아서 트윗에 있는 username이 사용자가 원하는 username과 동일한 것만 골라낸다
//username이 없는 경우라면 tweets를 할당한다
const data = await (username
? tweetRepository.getAllByUsername(username)
: tweetRepository.getAll());
res.status(200).json(data);
};
export async function getTweet (req, res, next) {
const id = req.params.id;
const tweet = await tweetRepository.getById(id);
if(tweet) {
res.status(200).json(tweet);
}
else {
res.status(404).json({message: "Tweet id(${id}) not found"});
}
};
export async function creatTweet (req, res, next) {
const { text, name, username } = req.body;
const tweet = await tweetRepository.creat(text, name, username);
res.status(201).json(tweet);
};
export async function updateTweet (req, res, next) {
const id = req.params.id;
const text = req.body.text;
const tweet = await tweetRepository.update(id, text);
if (tweet){
res.status(200).json(tweet);
}
else {
res.status(404).json({message: "Tweet id(${id}) not found"});
}
};
export async function deleteTweet (req, res, next) {
const id = req.params.id;
await tweetRepository.remove(id);
res.sendStatus(204);
};
5-3. 프론트앤드 코드 리팩토링
// client-Frontend/src/service/tweets/js
export default class TweetService {
// tweet API를 사용하는 프론트앤드 구현
constructor(http){
this.http = http;
}
async getTweets(username) {
const query = username ? `?username=${username}` : '';
return this.http.fetch(`/tweets${query}`, {
method: 'GET',
});
}
async postTweet(text) {
return this.http.fetch(`/tweets`, {
method: 'POST',
body: JSON.stringify({text,username: 'jaehyeok',name: 'Jaehyeok'}),
});
}
async deleteTweet(tweetId) {
return this.http.fetch(`/tweets${tweetId}`, {
method: 'DELETE',
});
}
async updateTweet(tweetId, text) {
return this.http.fetch(`/tweets/${tweetId}`, {
method: 'PUT',
body: JSON.stringify({ text }),
});
}
}
// client-Frontend/src/network/http.js
export default class HttpClient {
constructor(baseURL) {
this.baseURL = baseURL;
}
async fetch(url, options){
const res = await fetch(`${this.baseURL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
let data;
try {
data = await response.json();
} catch (error) {
console.error(error);
}
if (res.status > 299 || res.status < 200) {
const message = data && data.message ? data.message : 'Something Worng!'
throw new Error(message);
}
return data;
}
}
'Backend' 카테고리의 다른 글
디버깅 (0) | 2022.07.13 |
---|---|
node -buffer, stream (0) | 2022.07.12 |
node - os, process, path, fs (0) | 2022.07.12 |