909 Devlog

[유튜브 클론코딩] Pug 본문

토이 프로젝트/유튜브 클론코딩

[유튜브 클론코딩] Pug

구공구 2023. 7. 23. 03:01
728x90

1. Returning HTML

지금까지 response를 보낼 때, res.end()를 사용하여 response를 끝내버리거나, res.send()를 사용하여 간단한 문자열을 보내기만 했었습니다. 이제 HTML을 보내서 진짜 웹 페이지를 구성해 봅시다.

 

HTML을 보내는 세 가지 방법이 있습니다.

 

첫 번째 방법은 아래와 같이 res.send()에 HTML의 문자열을 써서 보내는 방법입니다.

res.send("<!DOCTYPE html><html lang='ko'><head><title>Document</title></head><body><h1>Hello World!</h1></body></html>");

이 방법을 사용하면 DOCTYPE부터 html, head, body 등등 모든 태그를 다 써서 보내야 합니다.

이 작업을 모든 res.send()에다가 전부 다 한다는 생각을 하기만 해도 끔찍합니다.

매우 귀찮고, 시간이 오래 걸리며, 어려운 작업입니다.

 

두 번째 방법은 HTML 파일 자체를 보내는 방법입니다.

res.sendFile(__dirname, 'index.html')과 같이 HTML 파일을 보내 바로 렌더링 할 수 있긴 합니다만, 이전 포스팅에서 썼던 request를 통해 각종 변수(req.params.id 같은)를 전달할 수 없을뿐더러, 큰 프로젝트를 진행해 보신 적이 있으시다면 느껴보셨겠지만 새로운 페이지를 만들 때 마다 HTML을 계속 복사 붙여넣기 해야하기 때문에 매우 복잡하고 귀찮습니다.

또, 아래에서 설명해 드릴 partial과 variable, conditonals, mixin 등 여러 좋은 기능을 두고 기본적인 HTML을 고집할 이유는 없어보입니다.

우리는 확장 가능하고, 매번 복붙하지 않도록 해주는 도구가 필요합니다.

 

세 번째 방법은 Template Engine을 사용하는 방법입니다.

1.1 Template Engine

템플릿 엔진은 Pug, ejs 등 여러가지가 존재합니다.

이번 포스팅에서는 Pug를 다루어 보겠습니다.

Pug는 Tamplate Engine으로써, HTML에 변수를 사용하는 등 더 편한 방법으로 HTML을 작성할 수 있습니다.

Pug를 설치해 봅시다.

터미널에 아래와 같이 입력해 줍니다.

npm i pug

이제 express에게 view engine으로 pug를 쓰겠다고 선언해야 합니다.

 

express의 시작점이 되는 파일, 저 같은 경우 server.js 파일에서 app.set()을 통해 선언합니다.

// server.js

...

const app = express();
const loggerMiddleware = morgan("dev");

app.set("view engine", "pug"); //<--- view engine으로 pug를 사용할 것을 선언

app.use(loggerMiddleware);

app.use("/", globalRouter);

...

위 사진과 같이 src 내부에 views 폴더를 생성하고, 그 안에 home.pug 파일을 생성했습니다.

 

이제 pug를 사용해 볼건데 그전에, express는 기본적으로 현재 작업 디렉토리의 하위 폴더들 중에서 views 폴더를 찾아 그 내부에 있는 파일을 사용합니다.

 

현재 작업 디렉토리는 node.js를 실행하는 디렉토리 기준이며 위 사진과 같은 파일 구성에서는 package.json이 있는 디렉토리가 현재 작업 디렉토리입니다.

 

따라서 express는 node-test > views를 찾고 있고,

위 사진과 같은 파일 구성을 사용하기 위해서는 node-test > src > views로 파일을 찾을 경로를 수정해야 하기 때문에 아래와 같이 경로 지정 코드를 추가합니다.

// server.js

...

const app = express();
const loggerMiddleware = morgan("dev");

app.set("view engine", "pug");
app.set("views", process.cwd() + "/src/views"); // <-- 파일 경로 지정
app.use(loggerMiddleware);

app.use("/", globalRouter);

...

이제 기본적인 Pug 파일을 작성해 봅시다.

1.2 기본적인 Pug 파일 생성

// home.pug

doctype html 
html(lang="ko")
    head 
        title PugTest 
    body 
        h1 Welcome to PugTest 
        footer &copy; 2023 PugTest

티스토리 코드블록에서 Pug를 지원하지 않아 JavaScript 타입으로 코드블록을 생성했습니다.

위 코드는 Pug이니 혼동하지 않으시길 바랍니다.

 

Pug의 문법은 {}(중괄호)를 사용하지 않습니다. 그 대신 들여 쓰기를 매우 유의하시면서 사용해야 합니다.

또, Pug는 HTML과 다르게 태그를 닫지 않습니다.

 

아래는 위 Pug 코드와 같은 형태의 HTML 코드입니다.

<!DOCTYPE html>
<html lang="ko">
    <head>
        <title>PugTest</title>
    </head>
    <body>
        <h1>Welcome to PugTest</h1>
        <footer>&copy; 2023 PugTest</footer>
    </body>
</html>

저희가 평소 사용했던 HTML 문법에서 크게 벗어나지 않는 문법으로, 익숙해지면 좀 더 편하게 사용하실 수 있을 것입니다.

 

이제 Pug 파일을 유저에게 보내봅시다.

 

1.3 send Pug File

사실 이 Pug 파일을 그대로 유저에게 보내는 건 아닙니다.

우리가 Pug로 파일을 보내면, Pug가 이 파일을 평범한 HTML로 변환한 뒤 유저에게 보이게 됩니다.

 

1에서 사용했던 res.send()가 아닌 res.render()를 사용해 응답합니다.

// videoController.js

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

...

이제 express가 views 폴더 내부에 있는 home 파일을 찾아 response를 보낼 것입니다.

 

이제 localhost:4000에 들어가 보면 아래와 같은 화면을 보실 수 있을 것입니다.

2. Pug

이제 Pug를 사용하는 진짜 이유들을 알아봅시다.

 

Pug는 자바스크립트이기 때문에 Pug 파일에서 변수를 사용하거나 자바스크립트 코드를 사용할 수 있습니다.

 

예를 들어, 위에서 사용했던 2023 PugTest라는 문장에서 시간이 바뀌어도 현재 년도를 나타나게끔 수정해 봅시다.

doctype html 
html(lang="ko")
    head 
        title PugTest 
    body 
        h1 Welcome to PugTest 
        footer &copy; #{new Date.getFullYear()} PugTest

#{}를 사용해 자바스크립트 코드를 사용할 수 있으며,

new Date().getFullYear()를 사용해 현재 년도를 표시합니다.

 

이제 다른 주소의 페이지를 표시하기 위해 watch.pug 파일을 만들어 봅시다.

// watch.pug

doctype html 
html(lang="ko")
    head 
        title PugTest 
    body 
        h1 Watch Video!
        footer &copy; #{new Date().getFullYear()} PugTest
// videoController.js

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

...

 

만약, footer에 현재 년도와, 월, 일을 표시하고 싶으면, 어떻게 해야 할까요?

모든 Pug 파일을 다 수정해야 할까요?

 

지금처럼 Pug 파일이 2개 있을 때는 괜찮겠지만 더 많아지면 너무 귀찮을 것 같습니다.

 

이럴 때를 위해 Pug에는 Includes라는 기능이 있습니다.

 

2.1 Includes

views 내부에 partials 폴더를 만들고, 그 내부에 footer.pug라는 파일을 만들어 봅시다.

partial은 일부분의 라는 뜻으로 여기서는 여러 파일에서 사용할 같은 형태의 pug의 한 부분을 뜻합니다.

예를 들어 header나 navbar, 그리고 밑에서 볼 footer 같은 공통 요소를 한 번만 작성해서 여러 파일에서 사용할 수 있습니다.

footer.pug에 위에 pug 파일에서 작성했던 footer 부분을 때서 넣어줍시다.

// footer.pug

footer &copy; #{new Date().getFullYear()} PugTest

 

이제 home.pug, watch.pug에 footer를 include 해봅시다.

 

// home.pug

doctype html 
html(lang="ko")
    head 
        title PugTest 
    body 
        h1 Welcome to PugTest 
    include partials/footer.pug
// watch.pug

doctype html 
html(lang="ko")
    head 
        title PugTest 
    body 
        h1 Watch Video!
    include partials/footer.pug

이제 footer.pug 파일 하나만 수정하면 footer.pug를 include 하는 모든 파일을 수정할 수 있습니다.

 

2.2 Template Inheritance

그런데, home.pug와 watch.pug 파일을 보면 구조가 h1의 내용 빼고는 같은 것을 볼 수 있습니다.

이 부분들도 footer처럼 간편하게 사용할 방법이 있을까요?

 

네, Pug에는 Inheritance(상속)이라는 기능이 있습니다.

상속은 일종의 베이스를 만드는데, 그 베이스를 바탕으로 하여 여러 페이지를 확장할 수 있는 기능입니다.

아래 내용을 따라서 해보시면 바로 감이 오실 겁니다.

 

views 폴더에 base.pug 파일을 만들었습니다.

base.pug에 home.pug의 내용을 복붙하고 home.pug와 watch.pug 파일의 내용을 모두 지우겠습니다.

// base.pug

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

 

이제 home.pug와 watch.pug에 base.pug를 extends(확장) 해 보겠습니다.

 

// home.pug

extends base.pug
// watch.pug

extends base.pug

이제 localhost:4000에 접속하면 extends base.pug 만 작성되어 있는 home.pug가 렌더링 되는데 전에 봤던 화면 그대로 표시되는 걸 볼 수 있습니다.

 

하지만 localhost:4000/videos/12에 접속해도 똑같이 home의 내용과 같은 내용이 표시되니 페이지마다 다른 내용을 표시할 수 있도록 해봅시다.

 

2.2.1 block

block은 템플릿에서 이 템플릿을 상속받을 자식 파일에서 따로 사용할 수 있는 코드 공간을 의미합니다.

block은 사용하고 싶은 곳에 block 블록명 과 같은 형식으로 사용합니다.

// base.pug

doctype html 
html(lang="ko")
    head 
        title PugTest
    body 
        block content // <-- block 사용
    include partials/footer.pug

이제 base.pug에 "content"를 위한 공간이 마련되었습니다.

 

이제 home.pug와 watch.pug에 블록에 들어갈 내용을 작성해 봅시다.

// home.pug

extends base.pug

block content 
    h1 Home!
// watch.pug

extends base.pug

block content 
    h1 Watch!

이제 주소마다 같은 base를 상속하고, 블록마다 다른 내용을 담을 수 있게 되었습니다.

inheritance의 다양한 예시는 pugjs.org-templateinheritance에서 볼 수 있습니다.

 

2.3 Variables

또, Pug에는 변수를 사용할 수 있습니다.

Pug에서 변수는 #{}=을 통해 사용할 수 있습니다.

 

#{}는 자바스크립트에서 사용하던 `${}`처럼 문자열 내부에 변수를 넣을 수 있고,

=은 태그에 변수 값만을 넣고 싶을 때 사용합니다.

 

head의 title에 페이지마다 다른 title을 나타나게 해 봅시다.

base.pug에 아래와 같이 수정해 줍니다.

// base.pug

doctype html 
html(lang="ko")
    head 
        title #{pageTitle} | PugTest // <-- #{}안에 변수 사용
    body 
    	header 
            h1=pageTitle // <-- = 뒤에 변수 사용
        block content
    include partials/footer.pug

이전에, #{} 안에 자바스크립트 코드를 사용할 수 있다는 것을 봤었습니다.

위 코드에서 pageTitle이란 변수는 자바스크립트 변수 같아 보이지만, 자바스크립트 변수가 아닙니다.

 

그렇다면 어떻게 변수를 사용할 수 있을까요?

 

정답은 우리가 response를 보낼 때 변수를 보내줘야 합니다.

videoController.js의 res.render() 함수에다가 보낼 pug 파일을 작성하고 그 뒤에 함께 보낼 변수도 작성합니다.

// videoController.js

export const trending = (req, res) => res.render("home", {pageTitle: "Home"});
export const watch = (req, res) => res.render("watch", {pageTitle: "Watch"});

이제 localhost:4000에 접속해 보면

head의 title과 body header의 h1에서 우리가 보낸 변수를 확인할 수 있습니다.

 


Pug에 대해 더 알아보기 전에 잠시 기본적인 CSS를 추가해 봅시다.

 

MVP Styles는 지금처럼 테스트를 하고 있는데, 기본적인 디자인을 넣어줄 때 유용하게 쓰입니다.

pug에 css를 적용하는 법은 아래와 같습니다.

// base.pug

doctype html 
html(lang="ko")
    head 
        title #{pageTitle} | PugTest
        link(rel="stylesheet" href="https://unpkg.com/mvp.css")
    body 
    	header 
            h1=pageTitle
        block content
    include partials/footer.pug

2.4 Conditionals

Conditionals는 if, else if 같은 조건문입니다.

 

조건문은 보통 회원가입 한 계정 / 회원가입 하지 않은 계정에 따라 다른 화면을 보여주고 싶을 때 사용하게 될 것입니다.

 

base.pug에 로그인 링크를 만들어 봅시다.

// base.pug

doctype html 
html(lang="ko")
    head 
        title #{pageTitle} | PugTest
        link(rel="stylesheet" href="https://unpkg.com/mvp.css")
    body 
        header 
            nav 
                ul 
                    li 
                        a(href="/login") Login // <-- 로그인 링크
            h1=pageTitle
        main 
            block content
    include partials/footer.pug

아직 데이터 베이스를 사용하기 전이니, videoController.js에 fakeuser를 만들어서 response에 유저 정보를 같이 보내봅시다.

// videoController.js

const fakeUser = {
    username:"Gugonggu",
    loggedIn: false,
};

export const trending = (req, res) => res.render("home", {pageTitle: "Home", fakeUser:fakeUser});
// export const trending = (req, res) => res.render("home", {pageTitle: "Home", fakeUser}); <-- ES6 문법
export const watch = (req, res) => res.render("watch", {pageTitle: "Watch"});

이제 fakeUser 객체에서 loggedIn이 true일 때 logout이 보이도록 만들어 봅시다.

// base.pug

doctype html 
html(lang="ko")
    head 
        title #{pageTitle} | PugTest
        link(rel="stylesheet" href="https://unpkg.com/mvp.css")
    body 
        header 
            nav 
                ul 
                    if fakeUser.loggedIn 
                        li 
                            a(href="/logout") Log out 
                    else
                        li 
                            a(href="/login") Login 
            h1=pageTitle
        main 
            block content
    include partials/footer.pug

이제 localhost:4000에 가보면

위와 같은 화면을 보실 수 있을 것입니다.

 

response에 보낸 fakeUser.loggedInfalse이기 때문에 if else 문을 거쳐 Login만 보이게 되었습니다.

 

이제 fakeUser 객체의 loggedIn을 true로 바꾸고 다시 접속해 보면

아래와 같이 Log out이 보이게 됩니다.

2.5 Iteration

iteration은 Pug에서 사용하는 반복문입니다.

Pug에서 반복문은 each와 while문이 있는데, 이번 포스팅에서는 each만 사용해 보도록 하겠습니다.

 

조건문에서와 마찬가지로, 아직 데이터 베이스를 사용하기 전이니, 가짜 video 배열을 만들어서 전달하고, 반복문을 통해 그 video들을 home 화면에 출력해 봅시다.

 

videoController.js에 배열을 만들고, response로 보내줍니다.

// videoController.js

export const trending = (req, res) => {
    const videos = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    return res.render("home", {pageTitle: "Home", videos});
};

home.pug에다가 각각의 videos 요소롤 출력하는 코드를 작성합니다.

// home.pug

extends base.pug

block content 
    h2 Welcome here you will see PugTest Homepage 
    ul 
        each video in videos 
            li=video

each문을 통해서, 각각의 videos 요소마다 li태그 안에 요소가 출력되었습니다.

 

만약 배열 안에 아무것도 들어있지 않다면 어떻게 될까요?

 

당연하게도 아무것도 출력되지 않을 것입니다.

하지만 이대로 두면 밋밋하니 배열이 비어있다고 출력되게 해 봅시다.

 

Pug에서는 each문 안에 else를 사용할 수 있습니다.

// home.pug

extends base.pug

block content 
    h2 Welcome here you will see PugTest Homepage 
    ul 
        each video in videos 
            li=video
        else 
            li Array is empty

우리가 의도한 것처럼, 배열이 비어있으니 Array is empty라는 문구를 출력하게 되었습니다.

 

iteration에 대한 다양한 예시를 보고 싶으시다면, pugjs.org-iteration에서 보실 수 있습니다.

 

2.6 Mixins

mixin은 2.1에서 봤던 partials와 비슷합니다.

하지만 mixin은 데이터를 받을 수 있는 똑똑한 partial을 의미합니다.

 

partial은 HTML의 한 조각을 나타내고, 데이터를 받지 못합니다 partial은 그저 똑같은 코드를 여기저기 복붙하고 싶지 않기 때문에 사용했었습니다.

 

만일 반복해서 등장하는 HTML 블록에서 서로 다른 데이터를 가져야 한다면 그때 mixin을 사용합니다.

 

mixin을 사용하기 전에, 위에서 만든 videos 배열을 그냥 num이 아닌 더미 video 데이터로 바꿔보겠습니다.

const videos = [
    {
        title: "First Video",
        rating:5,
        commnets:2,
        createdAt:"2 minutes ago",
        views:59,
        id:1,
    },
    {
        title: "Second Video",
        rating:5,
        commnets:2,
        createdAt:"2 minutes ago",
        views:59,
        id:1,
    },
    {
        title: "Third Video",
        rating:5,
        commnets:2,
        createdAt:"2 minutes ago",
        views:59,
        id:1,
    }
    ];

home.pug도 데이터에 맞춰서 수정해 주겠습니다.

// home.pug

extends base.pug

block content 
    h2 Welcome here you will see PugTest Homepage 
    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 
        li Array is empty

아래와 같이 더미 video 데이터가 화면에 출력됩니다.

만약 youtube.com에 가보신다면 home 페이지에 여러 영상들이 보일 것입니다.

영상 썸네일, 제목, 조회수, 업로더 등등이 보이고 그 영상을 클릭해 들어가면 오른쪽에 또 여러 영상들이 보일 것입니다.

 

즉, 그 영상들은 유튜브가 여러 곳에서나 재사용하고 싶어 하는 component이고, 우리가 그것을 만들어보려고 합니다.

 

지금 우리가 만든 페이지는 video 리스트를 보여주고 있습니다.

만약 영상을 클릭하게 된다면, 연관 video 리스트를 보여주고 싶을 것이고,

또 유저의 프로필에 가면, 그 유저의 video 리스트를 보여주고 싶을 것입니다.

 

따라서 

위 사진에서 드래그해놓은 부분을 복붙해서 다른 곳에서도 사용하게 될 것입니다.

 

이럴 때 mixin을 사용합니다.

 

views 폴더 안에 mixins 폴더를 생성하고 video.pug 파일을 생성했습니다.

video.pug 파일 안에 아래와 같이 작성합니다.

// video.pug

mixin video(info)
    div
        h4=info.title
        ul 
            li #{info.rating}/5.
            li #{info.comments} comments.
            li Posted #{info.createdAt}.
            li #{info.views} views.

또, mixin을 사용할 home.pug 파일을 아래와 같이 수정합니다.

// home.pug

extends base.pug
include mixins/video.pug // <-- include mixin

block content 
    h2 Welcome here you will see PugTest Homepage 
    each value in videos 
        +video(value) // <-- mixin 사용
    else 
        li Array is empty

mixin을 사용하기 위해서는 우선 mixin을 include 하고 +와 함께 함수처럼 사용하면 됩니다.

 

videos 안의 각각의 value에 대해서, video라는 mixin을 호출하고 info라는 객체를 보내고 있습니다.

 

이외에도 Pug는 편리함을 주는 다양한 문법들이 있으니 pugjs.org에 가셔서 한번 둘러보시는 것을 추천드립니다.

728x90