이 포스팅은 노마드 코더의 "유튜브 클론코딩" 강의를 바탕으로 작성되었습니다. https://nomadcoders.co
썸네일 이미지 출처: https://developers.google.com/youtube
2021년 7월 24일
지금까지는 사용자의 요청에 단순히 res.send()
로 문자열을 보냈습니다. 이제부터는 페이지 구성을 위해 HTML을 전송해보겠습니다.
우리가 보는 웹 페이지 안에는 반복되는 HTML 구조가 많습니다. 예를 들어 네비게이터나 푸터는 어느 페이지를 가도 보이기 때문에 순수한 HTML로 페이지를 구현한다면 HTML 간에 반복되는 코드의 양이 많아집니다.
붕어빵 틀로 붕어빵 모양을 만들고 팥이나 슈크림을 채워넣는 것과 같이 HTML도 반복되는 부분을 미리 틀로 만들어두고 페이지별로 필요한 세부 사항만 첨가할 수 있습니다. 이번 프로젝트에서는 pug를 사용해 그러한 작업을 해보겠습니다.
$ npm i pug
명령어로 설치합니다.server.js
파일에서 정의한 익스프레스 어플리케이션인 app
에 설정합니다.app.set("view engine", "pug"); // here
app.use(morgan("dev"));
app.use("/", globalRouter);
app.use("/videos", videoRouter);
app.use("/users", userRouter);
맨 윗 줄과 같이 app.set()
을 통해 뷰 엔진을 pug로 설정합니다.
src
폴더 내부터 views
폴더를 생성해주세요./src/views
폴더 안에 있다는 것을 알려줘야 합니다. 이 설정을 해주지 않으면 Expess는 view 파일을 프로젝트 폴더 최상위에서만 찾게 됩니다.app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views"); // here
app.use(logger);
app.use("/", globalRouter);
app.use("/users", userRouter);
app.use("/videos", videoRouter);
두 번째 줄과 같이 views
폴더의 위치를 Express에게 알려줍니다. process.cwd()
는 현재 작업 디렉토리를 반환합니다. 즉, 프로젝트 폴더 최상위 경로에 /src/views
를 더한 경로를 views
폴더의 위치라고 설정합니다.
위에서 생성했던 views
폴더 내부에 base.pug
파일을 생성합니다. 이 파일은 이제부터 프로젝트에서 사용할 HTML 파일의 템플릿이 됩니다. base.pug
파일에 다음 내용을 작성합니다.
doctype html
html(lang="ko")
head
title Home | Wetube
body
header
h1 Home
main
p I'm first pug file!
보이는 대로 pug 문법은 HTML보다 간편하게 만들어져 있습니다. HTML 문법을 알고 있다면 pug 문법은 무척 배우기 쉽습니다.
<>
와 닫는 태그 </>
가 없습니다.이제 만들어진 pug 파일을 렌더링해보겠습니다. 임시로 controllers/videoController.js
파일로 들어가서 trending
컨트롤러를 수정합니다.
export const trending = (req, res) => res.render("base");
send()
를 render()
로 바꿔주고 pug파일명을 확장자는 빼고 인자로 넘겨줍니다. trending
은 루트 URL을 담당하는 컨트롤러이므로 브라우저로 서버에 접속하면 바로 pug 파일이 HTML로 렌더링 된 것을 볼 수 있습니다.
그렇지만 pug 파일을 이렇게 쓰려고 다운받은 것은 아닙니다. 지금의 base.pug
파일은 어떤 페이지에서 렌더링하더라도 동일한 모습이기 때문입니다. 이제 pug 파일의 확장성에 대해 알아보겠습니다.
우선 footer
를 만들어보겠습니다. footer
나 nav
같은 경우 거의 모든 페이지에서 사용하는 필수 부품입니다. 따라서 한 페이지에 만들어두기보다는 부품으로 만들었다가 필요할 때 끼워 사용하는 것이 효과적입니다.
우선, views
폴더 안에 partials
폴더를 만듭니다. 그리고 그 안에 footer.pug
파일을 생성합니다.
footer.pug
파일을 간단하게 작성해줍니다.
footer © #{new Date().getFullYear()} Wetube
pug 파일에서 #{}
를 사용하여 자바스크립트 코드를 사용할 수 있습니다. new Date().getFullTear()
을 통해 footer
의 연도를 자동으로 업데이트 해줍니다.
이제 base.pug
파일에서 footer.pug
파일을 불러오겠습니다.
doctype html
html(lang="ko")
head
title Home | Wetube
body
header
h1 Home
main
p I'm first pug file!
include partials/footer
footer
가 들어갈 자리에 include partials/footer.pug
를 적어줍니다. pug 파일의 위치는 views
폴더가 기준입니다.
이제 브라우저에서 확인해보면 footer
가 제자리에 들어간 것을 볼 수 있습니다.
이렇게 pug 파일은 include
를 통해 다른 pug 파일을 사용할 수 있습니다.
이제 연습으로 edit
,home
, watch
pug 파일을 views
폴더 안에 만들어줍니다. 그리고 모든 파일에
extends base
를 적습니다. base.pug
템플릿을 확장해서 사용한다는 의미입니다.
그리고 videoController.js
에서 각각의 파일을 렌더링합니다.
trending 컨트롤러는 기존의 base 대신 home을,
see 컨트롤러는 watch를,
edit 컨트롤러는 edit을 렌더링해줍니다.
export const trending = (req, res) => res.render("home");
export const see = (req, res) => res.render("watch");
export const edit = (req, res) => res.render("edit");
이제 브라우저에서 각각의 URL로 들어가보면 모두 base.pug
가 렌더링되는 것을 볼 수 있습니다.
지금은 모든 페이지가 base.pug
파일을 똑같이 렌더링합니다. block
으로 공간을 지정해주면 해당 공간을 각자 파일에 맞게 꾸밀 수 있게 됩니다.
base.pug
의 main
태그 안쪽을 다음과 같이 content
라는 이름의 block
으로 지정해줍니다.
doctype html
html(lang="ko")
head
title Home | Wetube
body
header
h1 Home
main
block content
include partials/footer.pug
이제 content
라는 공간은 다른 파일에서 마음대로 꾸밀 수 있습니다. 이번에는 각각의 파일 별로 다른 h1
을 지정해주도록 하겠습니다.
home.pug
의 예입니다.
extends base
block content
h1 Welcome to the home!
content
블럭에 채울 내용을 적고 싶으면 block content
라고 똑같이 써준 뒤, 들여쓰기 후에 원하는 내용을 적으면 됩니다. 그러면 base.pug
파일의 content
블럭 안에 해당 내용이 들어가 HTML이 렌더됩니다.
doctype html
html(lang="ko")
head
title Home | Wetube
body
header
h1 Home
main
block content
include partials/footer.pug
base.pug
파일의 내용을 다시 한 번 보면 header
의 h1
부분은 Home으로 고정되어 있습니다. 이 역시 block
으로 만들어서 페이지 별로 다른 내용이 들어가게 할 수 있지만 미리 지정할 수 없는 값도 있습니다. 예를 들어, 유튜브에서 각 동영상의 제목을 표시하는 경우 크리에이터들이 만든 동영상의 제목을 개발자가 일일히 입력해줄 수는 없을 것입니다.
이럴 때 footer
의 연도를 표시할 때 썼던 #{}
를 활용하여 자바스크립트 변수를 사용할 수 있습니다.
doctype html
html(lang="ko")
head
title Home | Wetube
body
header
h1=pageTitle
main
block content
include partials/footer.pug
h1
부분을 다음과 같이 수정합니다. 이제 변수로 처리한 부분에는 자바스크립트의 pageTitle
이라는 변수가 들어가 HTML로 렌더링될 것입니다.
=
을 사용하는 것과 #{}
을 사용하는 것의 차이는 다른 문자열의 포함 여부입니다. 변수만 사용한다면 =
을 사용하여 변수값을 할당할 수 있고, 다른 문자열과 변수를 섞어 사용해야 한다면 #{}
를 써야 합니다.
이제 저 변수를 pug 파일에 전달해줘야 합니다. videoContoller.js
파일에서 각각의 컨트롤러에 pug 파일과 함께 변수를 담은 객체도 함께 전달해줍니다.
export const trending = (req, res) =>
res.render("home", {
pageTitle: "Home",
});
export const see = (req, res) =>
res.render("watch", {
pageTitle: "Watch videos",
});
export const edit = (req, res) =>
res.render("edit", {
pageTitle: "Edit video",
});
다음과 같이 pageTitle
변수를 객체로 전달합니다.
이제 브라우저에서 각각의 경로로 들어가보면 header
의 h1
이 페이지마다 다르게 나오는 것을 볼 수 있습니다.
localhost4000:/
localhost4000:/videos/1
localhost4000:/videos/1/edit
HTML 요소 중 특정 조건에 따라 다르게 보여주고 싶은 것이 있을 수 있습니다. 예를 들어, 로그인 버튼은 로그인 상태가 아닐 때 보여줘야 하고, 로그아웃 버튼은 로그인 상태일 때 보여줘야 합니다. 이런 기능을 pug에서 조건문으로 구현할 수 있습니다.
우선 pug 조건문을 연습하기 위해 가상의 유저와 로그인 상태를 videoController.js
상에 만들겠습니다.
const fakeUser = {
name: "Unknown",
isLogin: false,
};
가짜 유저 객체를 파일 맨 윗 줄에 만듭니다. 그리고 home.pug
파일에 pug 조건문을 만듭니다.
extends base
block content
h1 Welcome to the home!
if fakeUser.isLogin
h1 Hello #{fakeUser.name}
button Logout
else
button Login
조건문 끝에 콜론이 붙지 않는다는 것만 제외하면 파이썬의 조건문이랑 굉장히 유사합니다. pug에서는
가 조건문에서 쓰입니다. unless
의 경우 if !조건
과 같은 의미입니다.
이제 유저 객체를 videoController.js
의 trending
컨트롤러에 전달합니다.
export const trending = (req, res) => {
res.render("home", {
pageTitle: "Home",
fakeUser,
});
};
이제 fakeUser
의 isLogin
플래그에 따라 화면에 표시되는 내용이 달라지게 됩니다. isLogin
이 true
일 때는 로그아웃 버튼과 "Hello Unkown"이라는 문장이, false
일 때는 로그인 버튼이 보이게 됩니다.
isLogin: false
isLogin: true
이제 연습에 사용했던 유저 객체와 로그인 관련 코드는 지우고, pug에서 반복문을 사용하는 방법에 대해 알아보겠습니다.
화면에 100개의 비디오 썸네일과 제목을 만들어야 한다면 100개의 div
를 만드는 것보다는 비디오 객체를 전달받아 div
를 만드는 작업을 반복문에게 맡기는 것이 훨씬 편할 것입니다.
이번에는 pug 반복문을 연습해보겠습니다. 연습을 위해 videoController.js
파일 상단에 더미 비디오 객체 배열을 만듭니다.
const videos = [
{
title: "First video",
rating: 5,
comments: 43,
createdAt: "2 minuites ago",
views: 89,
id: 1,
},
{
title: "Second video",
rating: 5,
comments: 43,
createdAt: "2 minuites ago",
views: 89,
id: 2,
},
{
title: "Third video",
rating: 5,
comments: 43,
createdAt: "2 minuites ago",
views: 89,
id: 3,
},
];
그리고 home.pug
파일에 다음과 같이 반복문을 만듭니다.
extends base
block content
h1 Welcome to the home!
each video in videos
div
h4=video.title
ul
li #{video.rating}/5.
li #{video.comments} comments.
li Posted #{video.createdAt}.
li #{video.views} views.
else
h4 Sorry, no video.
pug 반복문은 each in
형태로 사용합니다. home.pug
가 전달받은 videos
객체 배열에서 각각의 비디오 객체를 꺼내 정보를 나열합니다.
pug 반복문에서는 else
를 사용할 수 있습니다. 만약 videos
가 빈 배열이라면 else
로 넘어가게 됩니다.
이제 아까와 같이 videos
리스트를 trending
컨트롤러에 전달합니다.
export const trending = (req, res) => {
res.render("home", {
pageTitle: "Home",
videos,
});
};
이제 홈페이지에 들어가보면 각각의 비디오 정보가 잘 출력되는 모습을 볼 수 있습니다.
pug로 코드 재사용성을 높일 수 있는 방법은 아직 더 있습니다. 예를 들어, 위에서 만든 비디오 반복문이 그렇습니다. 유튜브를 들어가보면 메인 화면에도 비디오 리스트가 있고, 비디오를 볼 때도 오른쪽 끝에 비디오 리스트가 있습니다. 즉, 비디오 리스트를 만들어주는 코드는 여러 페이지에서 필요로 합니다.
비디오 리스트를 만드는 코드를 재사용하기 위해 우선 views
폴더 안에 mixins
폴더를 만듭니다. 그리고 그 안에 video.pug
파일을 생성합니다.
mixin
을 만드는 것은 함수를 만드는 것과 유사합니다.
mixin video(video)
div
h4=video.title
ul
li #{video.rating}/5.
li #{video.comments} comments.
li Posted #{video.createdAt}.
li #{video.views} views.
mixin 이름(매개변수)
를 적습니다. 그리고 mixin
블럭 안에 home.pug
에서 비디오를 만들었던 틀을 복사합니다.
이제 home.pug
에서 mixin
을 불러옵니다.
extends base
include mixins/video
block content
h1 Welcome to the home!
each video in videos
+video(video)
else
h4 Sorry, no video.
mixin
을 불러올 때도 include
를 사용합니다. 파일 경로는 views
폴더가 기준입니다. 불러온 mixin
은 +이름(인자)
형태로 사용합니다. 브라우저를 통해 접속하면 이전과 동일한 결과를 볼 수 있습니다.
프로젝트를 진행하다보면 큰 틀부터 만들기 시작해서 세부 사항을 다듬어나가고, 스타일링은 거의 마지막 단계에 이르러 시작하게 됩니다. 그러나 그 마지막 단계까지 못생긴 HTML을 보고 있는 것은 고통이기도 합니다.
MVP.css 는 그런 고통을 조금 덜어줄 수 있습니다. MVP.css는 HTML 태그마다 심플한 스타일을 지정해놓은 CSS 파일입니다. 사용방법도 무척 간단합니다. base.pug
파일의 head
블럭 안에 밑의 코드를 붙여넣으면 됩니다.
link(rel="stylesheet" href="https://unpkg.com/mvp.css")
훌륭하다고는 할 수 없지만 그래도 이전보다 훨씬 보기 좋아졌습니다.
다음 링크에서 MVP.css에 대한 자세한 내용을 확인할 수 있습니다. https://andybrewer.github.io/mvp/
지금까지 Express의 pug를 사용한 HTML 템플릿 만들기의 기초에 대해 살펴보았습니다. 다음에는 유저 정보와 동영상을 저장할 데이터베이스의 기초에 대해 살펴보겠습니다.
<참고 문서>