[Node.js] MVC 패턴 알아보기

Yunhye Park·2023년 10월 27일
0
post-thumbnail

수업은 기능 위주로 진행되다 보니 한 폴더에 담기는 파일이 평균적 네다섯이다. 그래서 폴더를 구조화할 필요성이 현저히 적다. 하지만 프로젝트 단위로 일을 진행하다 보면 파일이며 코드가 기하급수적으로 늘어날 것이다. 한 폴더에 모든 기능을 한꺼번에 넣는다면 파일명으로 구분을 해야 할 텐데, 그것만으로는 구분이 쉽지 않을 거다.

애먼 곳에 에너지를 낭비하지 않으려면 폴더를 구조화하는 게 좋지 않을까? 오늘은 MVC, MVP, MVVM 등 다양한 디자인 패턴 중에서 MVC 패턴으로 기능을 나눠보았다.

MVC(Model View Controller)

디자인 패턴의 목적은 특정 기준으로 폴더를 생성하여 각 쓰임에 맞게 파일을 구분하기 위함이다. MVC라는 약자에 담겼듯 세 기능(데이터, 보여주기, 컨트롤)을 중심으로 파일을 분류한다. 다만 여기에 라우터도 함께 구조에 넣었다.

아래 도식화를 보자.

📍 user가 클라이언트를 통해 특정 라우터에 진입하면(request), 라우터는 컨트롤러에 해당 요청을 전달한다.

📍 화살표를 가만 보니 컨트롤러는 모든 폴더와 연결된다. 기능별로 나뉜 각 요소들을 컨트롤해서 요청에 적절한 응답을 클라이언트에 전달하는 것, 그게 컨트롤러의 역할이다.

📍 클라이언트는 단순 브라우저 접속뿐만 아니라 데이터베이스에 저장된 데이터를 필요로 할 수 있다. MySQL 등으로 작성했을 이 데이터베이스는 모델에 담겼다.

📍 컨트롤러가 데이터를 가져오거나 가공을 거친 후, 보여줄 화면에 관여한다. 바로 뷰다.

📍 사진에서는 일방적으로 뷰가 컨트롤러를 향하는 흐름이지만, 사실 컨트롤러와 모델의 관계처럼 양방향이다. user가 화면을 조작하며 전송한 데이터가 뷰를 통해 컨트롤러에 들어오고, 그 데이터와 기존 데이터를 (필요 시) 가공해 뷰에 전달할 수도 있기 때문이다.

MVC 패턴의 특징

  1. 설명만 보아서는 복잡해 보이지만, 디자인 패턴 중 가장 쉬운 축에 속한다. 그래서 보편적이다.
  2. 모든 과정을 컨트롤러를 통해 처리한다. 고로 뷰, 모델, 라우터는 서로 직접 소통하지 않는다.
  3. 그만큼 한 곳에 의존한다는 의미이기도 하다.

그래도 클라이언트의 요청과 응답 과정을 직관적으로 구분한 패턴이 아닌가 싶다.

예제 Login

이론을 백날 들어도 한번 해보는 것만큼 확실한 게 없다.

구현할 것

회원가입한 유저의 아이디, 패스워드, 이름 데이터를 데이터베이스에서 받아왔다. 이것과 user가 로그인 폼에 입력한 데이터를 비교해 제대로 입력했으면 이름을 부르는 greeting을, 그렇지 않으면 다시 확인하라고 잔소리를 해보자.

이걸 MVC 패턴으로 구조화해서 진행한다.

상황 가정

  • 폼 양식과 버튼 동작(greeting과 잔소리) 부분은 이미 구현해놨다.
  • 하필 데이터가 이런 식으로 넘어왔다.
let users =
`rocknroll//12341234//락앤롤
doremi//4321//도레미
une//1234//유네`;

처리 순서

  1. 라우터) 메인과 로그인, 404 등 라우터 분리
  2. 컨트롤러) 라우터에 연결할 컨트롤러 작성
  3. 모델) 유저 데이터 가공
  4. 컨트롤러) 가공한 데이터를 입력한 값과 비교하는 컨트롤러 작성 후 뷰에 전달
  5. 뷰) 라우터 연결
    (비교 결과를 어떻게 보여줄지는 미리 작성해서 생략)

MVC 폴더 구조

🖌 index.js

서버를 위한 기본 세팅을 해준다. express 모듈을 불러오고, ejs 템플릿 엔진 설정하고, body-farser도 사용하고, 포트도 연다.

const express = require("express");
const app = express();
const PORT = 8000;

app.set("view engine", "ejs");
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.listen(PORT, function () {
  console.log(`Sever Open: ${PORT}`);
});

그리고 라우터 처리도 해준다.

// 라우터 불러오기
const router = require('./routes/user')
// [세팅 엔트리] localhost:8000/user
app.use('/user', router)


app.get("*", (req, res)=>{
    res.send("페이지를 찾을 수 없습니다.");
})

routes/user.js에서 내보낸 모듈을 router라는 변수에 담았다. 받아온 모듈을 변수로 저장해 그대로 사용하는 걸 보면 파일 전역을 모듈로 내보낸 것 같다.

맞는 말인지 라우터 파일을 살펴보자.

🖌routes/user.js

express 모듈에 있는 Router 함수를 router라는 변수에 담았다. 이를 사용해 get과 post 방식으로 라우터를 생성했다. index.js에서 미들웨어(app.use)로 라우터의 엔트리를 '/user'로 작성했기 때문에 내부의 모든 주소는 '/user'이후로 간주한다.

const express = require('express');
const router = express.Router();
const controller = require('../controller/Cuser');

// localhost:8000/user
router.get("/", controller.main);
// localhost:8000/user
router.post("/login2", controller.login2);

module.exports = router;
module.exports = express.Router()

라우터를 두 개 만들었는데 각각을 변수에 담아 따로 내보내지 않고, 모든 라우터를 router라는 이름으로 한번에 내보냈다. 어차피 이 파일은 라우터를 위한 장소라서 모든 라우터가 동일한 미들웨어를 받아야 하기 때문이다.

라우터에 연결한 컨트롤러는 유저 데이터를 가공한 후에 완성할 수 있다. 고로 model 폴더를 먼저 살펴보자.

🖌 model/User.js

let users =
`rocknroll//12341234//락앤롤
doremi//4321//도레미
une//1234//유네`;

이 데이터를 [{}, {}, {}] 구조로 변형하면 데이터 비교에 용이할 것 같다. 그럼 split 메서드를 사용해 엔터를 기준으로 나눠주고, 빈 배열을 만들어 객체 형태의 데이터를 반복적으로(3개니까) push 하면 어떨까?

const userSplit = users.split("\n")

const userInfos = [];
for (const user of userSplit) {
    let parts = user.split("//")
    userInfos.push({
        id: parts[0],
        pw: parts[1],
        name: parts[2]
    })
}

module.exports = userInfos;

그렇게 작성해봤다.

이번엔 가장 마지막 줄부터 보자. 변수명 그대로 모듈을 내보냈다.

함수(exports.함수명 = () => {})로 모듈을 내보내면 모듈 받아올 때 :

const 작명 = require('함수모듈 있는 주소');

const 새작명 = 작명.여러 모듈 중에서 그 함수모듈()

로 진행한다. 물론 작명 대신 객체 구조분해({함수명})로 가져올 수도 있다. 그래도 함수는 호출해야 하니까.. 왠지 2단계를 거쳐야 하는 느낌이다. 하지만 변수로 내보내면 호출할 필요가 없으니 새로 담을 곳(변수)을 마련하지 않고 바로 작성할 수 있다.

게다가 다른 곳에 재사용하지도 않을 거라 함수로 안 묶어도 괜찮다고 보았다.

What if. 모듈 여럿을 내보내고 받을 때?

✔️ 객체 구조분해 : module.exports = {userInfos, 다른거}
✔️ 각각(module.)exports.userInfos = userInfos
(module.)exports.다른거 = 다른거

✔️ const 작명 = require('위 주소')으로 한방에 받아서 접근(작명.userInfos)
✔️ 혹은 const {userInfos, 다른거} = require('위 주소')로 객체분해 할당
(이 경우 여기서 가져온 변수명 그대로 사용)

💡 배열 반복문(순회), 무엇으로 작성할까?

데이터를 가공하면서 배열을 순회해야 했다. split으로 엔터 기준 배열을 만들었지만, 각 내용이 string으로 담겼다. 다행(?)인 건 나눌 기준이 존재한다는 거였다(//).

새 배열을 만들어서 그 안에 데이터를 push 할 생각이면 방법이 여럿(for of문, forEach, map 메서드 등)을 활용할 수 있다.

// forEach

const userInfos = [];
userSplit.forEach((user)=>{
	// 내부 동일
  })
// map()

const userInfos = [];
userSplit.map((user, i)=>{
	// 내부 동일
  })

반복에 쓰이는 여러 가지 간단 정리

  1. for은 데이터 타입에 구애받지 않고 작성할 수 있는 반복문이다.
  2. 그중에서 for of, for in문은 배열에서만 사용할 수 있다.
    둘의 차이점은 무엇을 기준으로 순회하느냐다.
  • for (const 요소 of 배열) : 배열을 인덱싱하여 요소 순회
  • for (const 인덱스 in 배열) : 배열의 인덱스넘버 순회
  1. 반복문과 특정 값 찾는 메서드(find, map, filter)는 엇비슷한 면이 존재한다.
  2. 하지만 find와 filter는 그 이름에서 보이듯 조건에 맞는 특정 값을 찾을 때 사용하는 게 적절하다.
  3. 게다가 find는 해당하는 첫번째 배열만 반환하기도 한다.

이렇게 데이터를 조회하기 쉬운 형식으로 바꾼 걸 모듈화해 내보내고, 컨트롤러에서 받아온다.

🖌 controller/Cuser.js

const userInfos = require('../model/User')

// localhost:8000/user/ 접속 시 index.ejs 렌더링 (GET 요청)
exports.main = (req, res) => {
    res.render('index')
}

// localhost:8000/user/login2에서 데이터 비교한 결과 보내기 (POST 요청)
exports.login2 = (req, res) => {
  let data;
  
  for (let i=0; i<userInfos.length; i++) {
    if (req.body.userid == userInfos[i].id
        && req.body.password == userInfos[i].pw) {
      data = {
        isSuccess: true,
        msg: `${userInfos[i].name}님 환영합니다!`
      }
    } else {
      data = {
        isSuccess: false,
        msg: '아이디와 비밀번호를 확인하세요.'
      }
    }
  }

  res.send(data);
}

옳은 데이터가 총 3가지라서 여기서도 반복문이 나온다. 배열의 크기(개수)만큼 순회해가며 user가 입력한 값과 같은지 비교하고 그 결과를 각각 true와 false로 구분해 메시지를 돌려준다.

T/F로 색상 구분 조건을, 메시지로 화면에 보여줄 내용을 정의했다. 바로 ejs 파일에.

🖌 views/index.ejs

  <body>
    <h2>실습 2. 로그인</h2>
    <form name="login">
      <fieldset>
        <legend>ID</legend>
        <input type="text" name="userid" />
      </fieldset>
      <fieldset>
        <legend>Password</legend>
        <input type="password" name="password" />
      </fieldset>

      <button type="button" onclick="clickLogin()">로그인</button>
    </form>
    <div class="login-result"></div>

    <script>
      function clickLogin() {
        const form = document.forms["login"];
        const data = {
          userid: form.userid.value,
          password: form.password.value
        }

        axios({
          method: "POST",
          url: "/user/login2",
          data: data
        }).then((res) => {
          const {isSuccess, msg} = res.data

          const element = document.querySelector(".login-result");
          element.innerHTML = msg;

          const color = isSuccess ? "blue" : "red";
          element.style.color = color
        })
      }
    </script>

폼과 버튼 이벤트를 미리 만든 가정이라서, method와 url만 확인하면 된다.

ejs 파일은 클라이언트와 연결된 프론트 영역이기에 데이터베이스에 있던 데이터는 이곳에서 절대 열람이 불가해야 한다. 그래서 받아온 데이터를 어디에 설정할지 각각의 메세지와 색깔만 확인할 수 있다.


가장 기억 남는 오류

🔸 어떤 값을 입력해도 로그인 메세지로 undefined가 떴다.

에러가 안 났다는 건 적어도 동작은 정확히 연결되었다는 거였다. 메세지는 전달 받은 데이터의 결과로 나오는 거니까.. 결국 데이터 부분에 문제가 있단 소리라는 생각이 들었다.

그래서 데이터 비교하는 컨트롤러 파일로 와서 쭉 살펴봤다.

exports.login2 = (req, res) => {
  let data;
  let flag;
  
  for (let i=0; i<userInfos.length; i++) {
    if (req.body.userid == userInfos[i].id
        && req.body.password == userInfos[i].pw) {
      data = {
        isSuccess: true,
        msg: `${userInfos[i].name}님 환영합니다!`
      }
      flag = false;
    }
 
  }
   if (!flag) {
      data = {
        isSuccess: false,
        msg: '아이디와 비밀번호를 확인하세요.'
      }
    }

  res.send(data);
}

🔸 나의 의도
else를 작성하지 않고,flag라는 새 변수를 만들었다. if문을 통과하면 flag 값을 false로 바꿔서 새 if문에 !flag를 조건으로 달아 로그인 실패 시의 data를 전송하려 했다.

하지만 로직 자체가 말이 안 되는 코드다. for문 안에 flag를 넣어버리면 언제나 실행되고, for문 밖에 두면 데이터가 맞을 때에도 flag가 바뀐다.

🔸 근데 왜 저렇게 생각했을까?
for문 메커니즘을 착각해서 저런.. 신기한 코드를 짰다.

인덱싱 0부터 입력 값 확인할 텐데 만약 입력한 값이 인덱싱 2에 있는 값이라면, else문으로 빠지는 줄 알았다. 근데 for문은 주어진 횟수만큼 다 반복한 후에야 else일 경우로 넘어간다.

생각해보니 당연히 그렇다. if-else문이면 당연히 한번에 모든 결과가 나올 테지만, 반복문이라는 건 주어진 조건 내에서 반복하나 뒤에 결과를 내온다는 거니까.


덧붙이는 말

🔆 수업 시간에 과제를 다 못 끝내고 집 간 게 처음이었다 ㅋ
🔆 원래 하려던 리액트 공부를 제쳐두고, 그날 집 와서 다시 풀었는데도 안 풀려서.. 나의 부족한 개념부터 파악했다.

  • 모듈 내보내고 가져오기
  • for loop

전자는 내가 블로그에 정리한 글 + 검색해가며 다시 공부했다.
후자는 유튜브에서 강의 3개 정도를 봤다. 문법과 쓰임을 아예 모르는 게 아닌데도 for과 if가 섞이면 헷갈리는 거 보면 잘 모르는 게 분명했다. 같은 주제로 다른 사람들이 설명하는 거 들으면 신기하게도 각자 다른 면을 채워준다.

🤔 오늘의 궁금증

라우터 폴더에서 라우터들을 모듈로 내보낼 때 module.exports = router로 내보내는데.. router는 결국 express 모듈에 있는 Router함수를 호출한 거라서 각각 get 요청과 post 요청으로 작성해도 router 하나로 퉁치는 걸까?

모듈 내보내기/가져오기가 가물가물해서 복습했는데도 저 부분은 아리까리하다. 여러 개를 내보낼 때 객체로 내보내서 새로운 식별자로 받거나 각각을 객체 구조분해 할당해서 내보내고 그 변수명 그대로 받거나 둘 중 하나로 배웠던 거 같은데..

음 router는 함수를 담아둔 식별자니까 그 함수를 통해 이어지는 다른 것들도 그 식별자에 딸려있는 걸로 인식하는 건가?

profile
일단 해보는 편

0개의 댓글