Zero To One

MVC 패턴 본문

Backend

MVC 패턴

Zero_To_One 2022. 7. 25. 17:08

1. MVC 패턴이란?

- 모델 (Model), 뷰(View), 컨트롤러 (Controller)로 이루어진 디자인 패턴

- 애플리케이션의 구성 요소를 3가지 역할로 구분하여 개발 프로세스에서 각각의 구성 요소에만 집중해서 개발 가능

- 장점 : 재사용성, 확장성

- 단점 : 애플리케이션이 복잡해질수록 모델과 뷰의 관계가 복잡해짐

https://developer.mozilla.org/en-US/docs/Glossary/MVC

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