[TIL] 211122

Lee Syong·2021년 11월 22일
0

TIL

목록 보기
96/204
post-thumbnail

📝 오늘 한 것

  1. routers - router 모듈화 / export default / import / 파라미터 / express 라우팅 / 정규식

  2. templates - pug / include


📚 배운 것

1. routers

'라우터(router) 만들고 사용하기'까지 함 ( [TIL] 211121 참고 )

import express from "express";
import morgan from "morgan";

const PORT = 4000;

const app = express();
const logger = morgan("dev");
app.use(logger);

const globalRouter = express.Router();
const handleHome = (req, res) => res.send("Home");
globalRouter.get("/", handleHome);

const userRouter = express.Router();
const handleEditUser = (req, res) => res.send("Edit User");
userRouter.get("/edit", handleEditUser);

const videoRouter = express.Router();
const handleWatchVideo = (req, res) => res.send("Watch Video");
videoRouter.get("/watch", handleWatchVideo);

app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/videos", videoRouter);

const handleListening = () =>
  console.log(`Server listening on port http://localhost:${PORT} 🎈`);

app.listen(PORT, handleListening);

1) router 모듈화

프로젝트를 계속 진행하다 보면 controller가 엄청나게 많아질 수 있기 때문에 애초에 controller와 router를 나누고자 한다.

(1) 모듈(module)이란?

모듈이란 독립적으로 재사용할 수 있는 소프트웨어 덩어리를 말한다.
프로젝트 안의 모든 파일들은 분리된 모듈로서 독립적이다.
즉, 하나의 파일 안에 있는 모든 것은 다른 파일로부터 완전히 private 상태이다.
따라서, a 파일에서 import 한 것과는 상관없이 b 파일에서도 그것이 필요하다면 b 파일에서도 import 해줘야 한다.

routers 폴더를 만든 후 globalRouter.js, userRouter.js, videoRouter.js 파일을 만들어 해당되는 코드를 옮겨주었다.

globalRouter.js

import express from "express";

const globalRouter = express.Router();

const handleHome = (req, res) => res.send("Home");

globalRouter.get("/", handleHome);

userRouter.js

import express from "express";

const userRouter = express.Router();

const handleEditUser = (req, res) => res.send("Edit User");

userRouter.get("/edit", handleEditUser);

videoRouter.js

import express from "express";

const videoRouter = express.Router();

const handleWatchVideo = (req, res) => res.send("Watch Video");

videoRouter.get("/watch", handleWatchVideo);

이렇게 하면 코드를 옮겨주고 남은 server.js 파일에서 콘솔에 globalRouter is not defined 에러가 뜬다.

server.js

import express from "express";
import morgan from "morgan";

const PORT = 4000;

const app = express();
const logger = morgan("dev");
app.use(logger);

app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/videos", videoRouter);

const handleListening = () =>
  console.log(`Server listening on port http://localhost:${PORT} 🎈`);

app.listen(PORT, handleListening);

에러를 해결하기 위해서는 globalRouter.js 파일의 globalRouter 변수를 server.js 파일로 import 해야 한다.
이를 위해서는 우선 globalRouter.js 파일에서 globallRouter를 export 해야 한다.

(2) export & import

각각의 ~router.js 파일 안에 해당 ~router 변수를 export 한 후, server.js 파일에서 import 한다.

globalRouter.js

export default globalRouter;

userRouter.js

export default userRouter;

videoRouter.js

export default videoRouter;

server.js

import globalRouter from "./routers/globalRouter.js"
import userRouter from "./routers/userRouter.js"
import videoRouter from "./routers/videoRouter.js"

모듈화 전과 마찬가지로 정상 작동하는 것을 확인할 수 있다.

그러나, 현재 각각의 ~router 파일 안에는 router 뿐만이 아니라 controller도 함께 들어가 있다.
controller는 함수이고 router는 그 함수를 이용하는 것으로 그 수가 많아지면 이 둘을 같은 파일에 넣는 것은 좋지 않다.

따라서, router와 controller를 분리해야 한다. controllers 폴더를 만들어보자.

(3) routers와 controllers 나누기

🔎 controllers 폴더 및 파일 만들기

controllers 폴더를 만든 후 userController.js, videoController.js 파일을 만들어 해당되는 코드를 옮겨주었다.

단, 이때 globalController.js는 만들 필요가 없다.
globalRouter 안에 포함되는 모든 url을 위한 controller 함수는 userController.js 혹은 videoController.js에 정의될 것이기 때문이다.
예를 들어, /join 페이지에서 회원가입을 하는 것은 user이고, /(Home) 페이지에서 보이는 것은 video이다.
globalRouter는 단지 url을 깔끔하게 하기 위해 쓰는 것일 뿐이다.

🔎 controller 함수 만들기

각각의 ~router.js 파일에서 controller 함수를 삭제한다.
userController.js와 videoController.js에 controller 함수를 다시 작성한다
이때 controller 함수의 이름에는 user, video를 굳이 붙여주지 않아도 된다. (이미 해당 파일 안에 있으므로)

🔎 export

각각의 ~controller.js 파일에서 모든 controller 함수를 export 한다

💡 export default 와 export 의 차이점

각각의 파일에서 export default는 한 번밖에 사용할 수 없다.
원하는 어떤 이름으로든 import 할 수 있다. (node.js가 그 파일의 default를 알고 어떤 이름으로든 변형시켜주기 때문이다.)

한편, 그냥 export는 파일 안에서 여러 번 사용할 수 있다.
반드시 export 한 이름으로 import 해야 한다.

🔎 import

이렇게 export 한 controller 함수를, 각각의 ~router.js 파일에서 import 한다

그냥 export를 사용하면, import 할 때는 { } 안에 이름을 적어줘야 한다.

만약 export 하지 않은 이름을 적으면, 콘솔에는 아래와 같은 오류가 뜬다.
라우터에 존재하지 않는 함수를 지정했다는 뜻이다.

Route.get() requires a callback function but got a [object Undefined]

(4) router 모듈화 완료

server.js

import express from "express";
import morgan from "morgan";
import globalRouter from "./routers/globalRouter";
import userRouter from "./routers/userRouter";
import videoRouter from "./routers/videoRouter";

const PORT = 4000;

const app = express();
const logger = morgan("dev");
app.use(logger);

app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/videos", videoRouter);

const handleListening = () =>
  console.log(`Server listening on port http://localhost:${PORT} 🎈`);

app.listen(PORT, handleListening);

globalRouter.js

import express from "express";
import { trending } from "../controllers/videoController";
import { join } from "../controllers/userController";

const globalRouter = express.Router();

globalRouter.get("/", trending);
globalRouter.get("/join", join);

export default globalRouter;

userRouter.js

import express from "express";
import { edit, remove } from "../controllers/userController";

const userRouter = express.Router();

userRouter.get("/edit", edit);
userRouter.get("/remove", remove);

export default userRouter;

videoRouter.js

import express from "express";
import { watch, edit } from "../controllers/videoController";

const videoRouter = express.Router();

videoRouter.get("/watch", watch);
videoRouter.get("/edit", edit);

export default videoRouter;

userController.js

export const join = (req, res) => res.send("Join");
export const edit = (req, res) => res.send("Edit User");
export const remove = (req, res) => res.send("Remove User");

videoController.js

export const trending = (req, res) => res.send("Home Page Videos");
export const watch = (req, res) => res.send("Watch Video");
export const edit = (req, res) => res.send("Edit Video");

모듈화 전과 마찬가지로 정상 작동하는 것을 확인할 수 있다.

🔥 정리하면,

server.js에서는 서버를 설정하고, router를 import 하고 있다.
각각의 ~router.js에서는 controller 함수를 import 하고 있다.

서버는 url이 어떻게 시작하는지에 따라 해당 router 안에 들어가서
url을 완성하고, controller 함수를 import 해서 실행한다

2) url 만들기

(1) url 추가하기 (리스트 작성)

홈페이지에 필요해 보이는 url을 가능한 한 모두 작성해본 후 각각의 controller 함수를 만들려고 한다. user 입장에서 생각해보자.

/

먼저 / (Home)으로 갈 것이다.
회원가입을 하기 위해 /join 으로 가거나 로그인을 하기 위해 /login 으로 갈 것이다.

/videos

그 후, 동영상을 검색하러 /search 로 갈 것이다.
이제 동영상을 보러 들어갈 것이다.
/videos/watch 를 /videos/:id 로 수정한다.
여기서 id는 동영상의 id를 뜻한다.

한편, 동영상을 업로드하기 위해 /video/upload 로 갈 수 있다.
업로드한 동영상을 시청하고, 수정하고, 삭제할 수 있다.
이때 누구나 동영상을 시청할 수 있다.
또한, 누구나 로그인을 하면 동영상을 업로드할 수 있다.
그러나, 동영상을 수정 및 삭제는 해당 동영상을 업로드한 사람만이 할 수 있다.
(댓글 관련해서는 나중에 생각하도록 하자. 일단은 지워준다.)

/users

user는 프로필을 볼 수 있다.
user는 자신의 프로필을 보고, 수정하고, 삭제할 수 있다.
이때 누구나 프로필을 볼 수 있다.
그러나, 프로필 수정 및 삭제는 해당 프로필 user만이 할 수 있다.

user는 로그아웃을 하기 위해 /users/logout 으로 갈 것이다.

✔ 이를 url로 정리하면 아래와 같이 작성할 수 있다.

/ → Home
/join → Join
/login → Login
/search → Search

/user/:id → See user
/user/logout → Log Out
/users/edit → Edit My Profile
/users/remove → Remove My Profile

/videos/:id → See video
/videos/:id/edit → Edit video
/videos/:id/remove → Remove video
/videos/upload → Upload Video

(2) router & controller 만들기

위에서 작성한 url 리스트를 바탕으로 router와 controller를 만든 후 export & import 해보자. 앞에서 해준 것과 똑같이 한다.

/users 에서 /:id 는 일단 그대로 적은 후에 가장 마지막으로 옮긴다. (뒤에서 설명)

(3) 파라미터(parameters)

💡 파라미터란 무엇인가?

videoRouter.get("/videos/:id", see);

위 코드에서 :id를 파라미터라고 부른다.
파라미터를 사용하면, url 안에 변수를 넣을 수 있다.
:는 express에게 뒤에 나오는 텍스트가 변수임을 알려주기 위해 반드시 붙여야 하지만
변수는 당연히 id 말고 다른 변수를 사용할 수 있다.

예를 들어, 사용자가 브라우저의 주소 창에 /videos/333 을 입력했다고 하자.
express는 video 라우터 안으로 들어가서 사용자가 입력한 url의 나머지 주소(즉, /333)를 찾는다.
이때 express는 이 숫자 333을 위 코드의 :id 안에 넣어준다.
서버는 /videos/333 을 인식할 수 있게 되므로 see controller 함수를 import 해 실행한다.

💡 코드에서 액세스 되는 과정

이 과정을 확인하기 위해 req.params를 출력해보자. videoController.js 파일을 아래와 같이 수정했다.

export const see = (req, res) => {
  return res.send(`See Video #${req.params}`);
};

export const edit = (req, res) => {
  return res.send(`Edit Video #${req.params}`);
};

브라우저의 주소 창에 localhost:4000/video/3423 혹은 localhost:4000/video/3423/edit을 입력해 들어가면 콘솔 창에 { id: 3423 } 이 찍히는 것을 확인할 수 있다.
express가 우리가 사용한 파라미터 이름과 함께 그 값을 출력해주는 것이다.

이렇게 파라미터를 사용하면, video 라우터 파일에 모든 동영상 url을 적어줄 필요가 없어 매우 편리하다.

💡 /upload 를 /:id 보다 위에 작성한 이유는 무엇일까?

반대로 /:id 를 /upload 보다 위에 작성해서는 안되는 이유를 알아보자.

videoController.js 파일을 다시 한 번 아래와 같이 수정할 수 있다.

export const see = (req, res) => {
  return res.send(`See Video #${req.params.id}`);
};

export const upload = (req, res) => res.send("Upload Video");

이때 videoRouter.js 파일에서 코드를 작성한 순서가 아래와 같다고 하자.
즉, /:id 가 /upload 보다 먼저 온다.

videoRouter.get("/:id", see);
videoRouter.get("/upload", upload);

먼저, 주소 창에 localhost:4000/videos/24 라고 입력해 들어가면 화면에는 See Video #24 라고 뜬다.
그런데 문제는 localhost:4000/videos/upload 라고 입력해 들어가도 화면에 See Video #upload 라고 뜬다는 것이다.

express는 코드를 순서대로 읽는데 express 입장에서는 /:id 가 위에 있기 때문에 같은 형식의 url이라면 그 url을 찾기 위해 굳이 그 밑의 /upload로 넘어가지 않는다.

즉, localhost:4000/videos/upload가 입력되었든 localhost:4000/videos/24가 입력되었든 videoRouter.get("/:id", see) 코드를 읽는 것이다.

따라서 express는 localhost:4000/videos/upload의 upload를 텍스트가 아니라 변수로 봄으로써 화면에 See Video #upload를 띄우게 된다.

이러한 문제를 방지하기 위해서 /upload 를 /:id 보다 먼저 오게끔 해야 한다.
이렇게 해야 express가 코드를 순서대로 읽으면서 upload를 변수가 아닌 텍스트로 인식하여 upload controller가 실행됨으로써 화면에는 Upload Video 가 뜰 것이다.

videoRouter.get("/upload", upload);
videoRouter.get("/:id", see);

(4) express 라우팅

express - 라우팅 中 라우팅 경로 참고

한편, 이렇게 코드를 작성해도 localhost:4000/videos/eeee 등 url에 upload가 아닌 다른 문자열이 포함되어 있다면 화면에는 See Video #eeee 등이 뜨게 된다.

이 문제를 해결하기 위해 express에게 :id에 숫자만이 와야 한다고 전달해야 한다.
이를 express 라우팅을 이용할 수 있다.
그 전에 먼저 정규식에 대해 알아보자.

💡 정규식(Regular Expression)

Regex Tester를 이용할 수 있다

정규식이란 문자열로부터 특정 정보를 추출해내는 방법을 말한다.
정규식은 모든 프로그래밍 언어에 존재한다.

  • 특정 단어로 시작하는 어떤 단어든 모두 선택하는 정규식

[ 예제 ]

syong으로 시작하는 어떤 단어든 모두 선택하는 정규식 : (syong\w+)

Hello my name is syonglee and im 58, my name is also syongsyong

→ 여기서 \w란 단어(word)를 의미한다. +란 끝까지 모두 선택함을 의미한다.

  • 숫자만 선택하는 정규식

[ 예제 ]

(\d+)

/videos/12
/videos/lalalalala

→ 여기서 \d란 숫자(digit)을 의미한다.

💡 express 라우팅

정규식을 바탕으로 videoRouter.js 파일을 수정할 수 있다.
자바스크립트에서는 (\d+)이 아니라 (\\d+)라고 써야 한다.
파라미터 이름 뒤에 (\\d+)를 추가한다.
( 파라미터 이름을 지우고 정규식만 써도 실행은 되지만, 이렇게 하면 controller 함수에서 파라미터의 id 값을 불러오지 못한다. )

videoRouter.get("/:id(\\d+)", see);
videoRouter.get("/:id(\\d+)/edit", edit);
videoRouter.get("/:id(\\d+)/remove", remove);
videoRouter.get("/upload", upload);

이제 :id에 숫자가 아닌 것은 올 수 없다.
따라서, localhost:4000/videos/eeee 에 들어가면 화면에는 더 이상 See Video #eeee 가 뜨지 않고 Cannot GET /videos/eeee 라고 뜬다.
또한, /upload 를 /:id 보다 뒤에 작성해도 문제가 없다.

다만, 실제로 이 프로젝트에서 위와 같은 정규식을 사용하지는 않을 것이다. 데이터베이스가 다른 형식이기 때문이다.


2. Templates

1) 문자열 대신 HTML return 하기

지금은 controller 함수들이 각각 문자열만을 return 하고 있다.
대신에 HTML을 return 해보자.

문자열이 들어간 자리에 그대로 HTML을 작성하는 방법으로 HTML을 return 할 수 있다.
예를 들어, 아래와 같이 작성할 수 있다.

export const trending = (req, res) => res.send("<h1>Home</h1>");

그러나 이렇게 하면 방대한 HTML 코드를 담기엔 무리가 있고, 유지·보수하기도 어렵다.
이 문제를 해결하기 위해 pug를 사용할 수 있다.

2) pug

(1) pug란?

pug란 템플릿을 이용해 뷰(view)를 만드는 것을 돕는 템플릿 엔진(Template Engine)을 말한다.

예를 들어, 아래와 같이 작성하면

doctype html
html(lang="en")
  head
    title= pageTitle
  body
	#container.col

이렇게 바꿔준다.

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Pug</title>
  </head>
  <body>
    <div id="container" class="col">
    </div>
  </body>
</html>

(2) 설치 및 설정

우선, npm을 이용해 pug를 설치한다.

npm i pug

server.js 파일에서 app.set()을 추가해 express가 view engine으로 pug를 사용하도록 설정한다.

app.set("view engine", "pug");

(3) 사용

💡 home.pug 파일 만들기

이제 express는 HTML을 return 하는 데 pug를 사용한다.

기본적으로 express는 현재 작업 디렉토리(current working directory: cwd)의 views라는 디렉토리 안에서 pug 파일을 찾는다.

이 점을 기억하고, 여기서는 일단 src 폴더 안에 views라는 폴더를 만든 후 home.pug라는 파일을 만들도록 한다.

cf. 여기서 views란 애플리케이션의 뷰(view)에 대한 디렉토리나 디렉토리 배열을 말한다.
뷰(view)나 템플릿, HTML은 모두 'user가 보는 대상'으로 같은 것을 의미한다.

작성 규칙

  • 모든 건 소문자로 작성한다
  • 속성이 있으면 괄호 안에 작성한다
  • 자식은 부모보다 안쪽에 있어야 한다 (들여쓰기)

[ home.pug ]

doctype html
html(lang="ko")
  head
    title Wetube
  body
    h1 Welcome to Wetube
    footer &copy: 2021 Wetube

💡 pug 파일 렌더링 : res.render("view 이름")

pug는 이렇게 작성한 home.pug 파일을 렌더링하여 HTML 파일로 변환한다. user는 이렇게 변환된 HTML을 보게 된다.

이를 위해 controller 함수가 변환된 HTML 파일을 return 하도록 아래와 같이 수정해야 한다. res.render()의 인자로 view의 이름을 넣어준다.

export const trending = (req, res) => res.render("home");

(4) 현재 작업 디렉토리(cwd)

그런데 여기까지 진행한 후 localhost:4000 으로 들어가면, 아래와 같은 에러가 뜬 걸 확인할 수 있다.

Error: Failed to lookup view "home" in views directory "/home/syong/projects/wetube-reloaded/views"

"~wetube-reloaded/views"에서 "home"이라는 view를 찾는 데 실패했다는 뜻이다.
이는 기본적으로 정상 작동하기 위해서는 views 폴더를 현재 작업 디렉토리 안에 만들어야 하는데 src 폴더 안에 만들었기 때문에 발생한 오류이다.

console.log(process.cwd()) 를 통해 현재 작업 디렉토리를 확인할 수 있다.
현재 작업 디렉토리는 서버를 기동하는(node.js를 실행하는) 파일의 위치에 따라 결정된다.

서버를 기동하는 파일은 package.json이고, 이 파일은 wetube 폴더 안에 있다.
따라서, 이 프로젝트의 경우 현재 작업 디렉토리는 wetube 폴더이므로 기본적으로 views 폴더는 wetube 폴더 안에 위치해야 에러가 발생하지 않는다.

에러를 해결하기 위해서는 ① views 폴더를 wetube 폴더로 옮기거나 ② server.js 파일에 다음 코드를 추가하여 기본값을 바꿔야 한다. 참고로 기본값은 process.cwd() + "/views" 라고 되어 있다. ( Application Settings 참고 )

app.set("views", process.cwd() + "/src/views");

(5) include

pug의 최대 장점은 반복을 할 필요 없다는 것이다.
footer를 예시로 들어 설명해보겠다.

💡 #{자바스크립트 코드}

그 전에 pug에서 변수를 사용하거나 자바스크립트 코드를 사용하기 위해 #{자바스크립트 코드}처럼 쓸 수 있음을 알고 넘어가자.

💡 see.pug 파일 만들기

지금까지 home.pug 파일을 만들어서 렌더링했다.
이제 views 폴더 안에 see.pug 파일을 추가해보자.

doctype html
html(lang="ko")
  head
    title Wetube
  body
    h1 See Video
    footer &copy: #{new Date().getFullYear()} Wetube

마찬가지로 see.pug 파일을 렌더링하여 변환한 HTML을 화면에 보여줄 수 있다.

💡 footer

그러나 여기에는 반복되는 부분이 있다.
see.pug 파일에는 footer가 있는데, 만약 모든 페이지에 같은 footer를 넣고자 한다면, footer를 한번 수정할 때마다 모든 pug 파일 안의 footer를 일일이 수정해야 한다.
이는 비효율적이다. 이를 해결하기 위해 include를 사용할 수 있다.

🔥 partials

views 폴더 안에 partials 폴더를 만든 후, 그 안에 footer.pug 파일을 추가한다.
home.pug와 see.pug 파일에서는 footer를 삭제한다.

 footer &copy: #{new Date().getFullYear()} Wetube

화면에 footer를 띄우기 위해 footer.pug 파일을 include 해야 한다. ( pug - includes 참고 )
include를 이용하면, 어떤 pug 파일의 내용을 다른 pug 파일에 포함시킬 수 있다.

home.pug와 see.pug 파일을 아래와 같이 수정한다.

doctype html
html(lang="ko")
  head
    title Wetube
  body
    h1 Welcome to Wetube
    include partials/footer.pug 🔥
doctype html
html(lang="ko")
  head
    title Wetube
  body
    h1 See Video
    include partials/footer.pug 🔥

이제 localhost:4000 와 localhost:4000/video/342 에 들어가면 같은 footer를 보여주는 걸 확인할 수 있다. footer.pug 파일만 수정하면 자동으로 모두 업데이트 된다.

💡 정리하면

  • pug를 이용하면 깔끔한 HTML 코드를 작성할 수 있다.
  • HTML에 자바스크립트를 포함시킬 수 있다. → #{자바스크립트 코드}
  • 하나의 파일만 수정하면 모든 템플릿을 업데이트 할 수 있다. → include

✨ 내일 할 것

  1. 강의 계속 듣기
profile
능동적으로 살자, 행복하게😁

0개의 댓글