갈루아의 반서재


Haskell Yesod의 템플릿 언어 세익스피어를 활용하여 웹페이지 만들기 (2)



이전 포스팅에서는 헤더와 푸터 영역의 작성에 대해서 알아봤습니다. 현시점에서 샘플사이트의 모습은 대략 이런 느낌일 것입니다.

이번 회차에서는 아래의 부분을 완성해보겠습니다.

  • 슬라이드쇼
  • 메인 컨텐츠

템플릿 파일에 대해서


templates/default-layout-wrapper

이 파일에는head태그에 포함된 meta태그와 title태그 및 닫힌 body 태그의 앞에 배치된 스크립트 등을 기술합니다. 사이트 전체의 레이아웃 헤더, 푸터, 사이드바 등의 구조)는 기술하지 않고, 해당 정보는 차후에 언급할 templates/default-layout 에 기술합니다.

templates/default-layout

이 파일에서는 사이트의 레이아웃(헤더, 푸터, 사이드바 등)을 기술합니다. 일반적으로 default-layout-wrapper 가  default-layout 을 끼워넣어 처리하고, default-layout 가 각각의 핸들러에 대응하는 파일을 처리합니다.  이번에는 config/routes 파일을 살펴보고, /의 경로에 대응하는 핸들러는 HomeR 가 있음을 알아봅니다.

1
2
3
4
5
6
7
8
/static StaticR Static appStatic
 
/favicon.ico FaviconR GET
/robots.txt RobotsR GET
 
/ HomeR GET POST


cs


/ 로 접근할 때의 동작을 정의하기 위해 Handler/Home.hs 파일의 내용을 편집해봅니다. 샘플 프로그램의 핸들러는 여러가지로 정의될 수 있는데, 아래와 같이 수정해봅니다.

1
2
3
4
5
6
7
8
module Handler.Home where
 
import Import
 
getHomeR :: Handler Html
getHomeR = defaultLayout $ do
  setTitle "Yesod チュートリアル!!!"
  $(widgetFile "homepage")
cs

실행해보면 아래와 같은 에러가 발생합니다.

1
2
3
Application.hs:39:1: Not in scope: ‘postHomeR’
Build failure, pausing...
Rebuilding application... (using cabal)
cs

이것은config/routes 에는 / HomeR GET POST 와 같이 기술이 되어있어, Handler/Home.hs 에는 getHomeR 과 postHomeR 양방의 정의를 모두 기대하고 있지만, 실제로는 getHomeR 밖에 정의되어 있지 않기 때문에 해당 에러가 발생하는 것입니다. 이것을 수정하기 위해서는 아래의 2가지 방법이 있습니다.

  1. config/routes 를 수정,GET 메서드만 남긴다
  2. Handler/Home.hs 을 수정、postHomeR 를 정의한다

여기서는 POST 메서드가 필요하지 않으므로, 1의 방법을 따릅니다. 

1
2
3
4
5
6
7
8
/static StaticR Static appStatic
 
/favicon.ico FaviconR GET
/robots.txt RobotsR GET
 
/ HomeR GET


cs


이상으로 에러는 해소되었습니다. 조금전의 Handler/Home.hs 에는 $(widgetFile "homepage") 라고하는 익숙하지않은 표현이 있었습니다. 이것은 Scaffolded 사이트에서 사용되는 편리한 함수의 하나입니다. 

템플릿 Haskell과 같이 되어 있어기본적으로 아래에 표시되는 확장자를 가지고 있으며, 인수에는 지정된 이름의 파일을 읽어넣게 됩니다.

  • .hamlet
  • .cassius
  • .lucius
  • .julius

hamletFile 등과 달리 widgetFile 은 암묵적으로 templates 디렉토리 아래에서 찾을 수 있습니다. 또한 확장자는 지정할 수 없습니다. 이번 예의 경우에는

  • templates/homepage.hamlet
  • templates/homepage.cassius
  • templates/homepage.lucius
  • templates/homepage.julius

의 가운데 실제로 존재하는 파일 전체를 읽어들이게 됩니다. 또한 이 widgetFile 은 커스토마이징이 가능합니다.

슬라이드쇼

그러면 슬라이드쇼를 정의해보겠습니다.

templates/homepage.cassius

1
2
3
4
5
.navbar
  margin-bottom: 0
 
.carousel-inner img
  width: 100%
cs


templates/homepage.hamlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div #carousel-example-generic .carousel .slide data-ride="carousel">
  <ol .carousel-indicators>
    <li data-target="#carousel-example-generic" data-slide-to="0" .active></li>
    <li data-target="#carousel-example-generic" data-slide-to="1"></li>
    <li data-target="#carousel-example-generic" data-slide-to="2"></li>
  <div .carousel-inner>
    <div .item .active>
      <img src="img/slide-1.jpg" alt="slide-1">
      <div .carousel-caption>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit.
    <div .item>
      <img src="img/slide-2.jpg" alt="slide-2">
      <div .carousel-caption>
        A dolorem et ipsa doloremque tempore placeat voluptates repellat repudiandae autem? Eius,
    <div .item>
      <img src="img/slide-3.jpg" alt="slide-3">
      <div .carousel-caption>
        explicabo corporis eveniet ipsum velit labore quas voluptatum exercitationem fugit.
    <a .left .carousel-control href="#carousel-example-generic" data-slide="prev">
      <span .glyphicon .glyphicon-chevron-left>
    <a .right .carousel-control href="#carousel-example-generic" data-slide="next">
      <span .glyphicon .glyphicon-chevron-right>
cs


templates/homepage.hamlet 과 templates/homepage.cassius 에 상기 내용을 기술합니다. 그렇지만 아무 것도 반영되지 않습니다. default/layout.hamlet 에 핸들러의 위젯을 넣어서 처리하지 않았기 때문입니다.

templates/default-layout.hamlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<div #header>
  <div .logo>
    <h1>LOGO
  <nav .navbar .navbar-default role="navigation">
    <div .container>
      <div .navbar-header>
        <button .navbar-toggle data-toggle="collapse" data-target="#bs-example-navbar-collapse-1">
          <span .sr-only>Toggle navigation
          <span .icon-bar>
          <span .icon-bar>
          <span .icon-bar>
        <span .navbar-brand .visible-xs href="@{HomeR}">Menu
      <div .collapse .navbar-collapse #bs-example-navbar-collapse-1>
        <ul .nav .navbar-nav>
          $forall n <- [1,2,3,4,5]
            $if n == 1
              <li .first>
                <a href=@{HomeR}>MENU#{show n}
            $else
              <li>
                <a href=@{HomeR}>MENU#{show n}
 
^{widget}
 
<div #footer>
  <div .logo>
    <p>LOGO
  <ul .navbar-nav .list-inline>
    $forall n <- [1,2,3,4,5]
      $if n == 1
        <li .first>
          <a href=@{HomeR}>MENU#{show n}
      $else
        <li>
          <a href=@{HomeR}>MENU#{show n}
  <div .clearfix>
  <ul .sns-icon .list-inline>
    <li>
      <i .fa .fa-twitter .fa-2x>
    <li>
      <i .fa .fa-facebook .fa-2x>
  <div .copy>
    <span>#{appCopyright $ appSettings master}
cs

정말 단순합니다. ^{widget} 을 넣었을 뿐입니다. 이 widget 이라고 하는 함수는 Foundation.hs 를 

보면 알수있는바와 같이, defaultLayout 의 인수입니다.

이미지 처리

슬라이드쇼에 활용될 적당한 이미지를 준비합니다. 준비가 되었으면 static 아래에 위치한 images 라고 불리는 이름의 새로운 디렉토리를 만들고 여기 이미지 파일을 배치합니다. 필자의 경우 아래와 같이 명명된 이미지 파일들을 사용하도록 합니다.

  • static/images/images1.jpg
  • static/images/images2.jpg
  • static/images/images3.jpg

그리고 templates/homepage.hamlet 의 img 태그의 src 속성을 아래와 같이 형안전 URL 을 참조하여 수정합니다.

templates/homepage.hamlet


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div #carousel-example-generic .carousel .slide data-ride="carousel">
  <ol .carousel-indicators>
    <li data-target="#carousel-example-generic" data-slide-to="0" .active></li>
    <li data-target="#carousel-example-generic" data-slide-to="1"></li>
    <li data-target="#carousel-example-generic" data-slide-to="2"></li>
  <div .carousel-inner>
    <div .item .active>
      <img src=@{StaticR images_images1_jpg} alt="slide-1">
      <div .carousel-caption>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit.
    <div .item>
      <img src=@{StaticR images_images2_jpg} alt="slide-2">
      <div .carousel-caption>
        A dolorem et ipsa doloremque tempore placeat voluptates repellat repudiandae autem? Eius,
    <div .item>
      <img src=@{StaticR images_images3_jpg} alt="slide-3">
      <div .carousel-caption>
        explicabo corporis eveniet ipsum velit labore quas voluptatum exercitationem fugit.
    <a .left .carousel-control href="#carousel-example-generic" data-slide="prev">
      <span .glyphicon .glyphicon-chevron-left>
    <a .right .carousel-control href="#carousel-example-generic" data-slide="next">
      <span .glyphicon .glyphicon-chevron-right>
cs


정적 파일을 취급하는 방식은 @{StaticR images_images1_jpg} 와 같은 형식입니다. 디렉토리 구조를 나타내는 / 와 확장자는 _ 로 표현됩니다. 즉, images 폴더에 위치한 images.jpg 파일이라는 의미입니다.

1
(blackbriar) root@gcloudx:~/blackbriar/blackbriar/src# touch Settings/StaticFiles.hs
cs


마지막으로 슬라이드쇼를 마무리 하기 위해jQuery 와 bootstrap 의 스크립트파일을 불러옵니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
module Handler.Home where
 
import Import
import Yesod.Static (staticFiles)
 
staticFiles (appStaticDir compileTimeAppSettings)
 
getHomeR :: Handler Html
getHomeR = defaultLayout $ do
  setTitle "Yesod チュートリアル!!!"
  addScriptRemote "https://code.jquery.com/jquery.js"
  addScriptRemote "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
  $(widgetFile "homepage")
cs


현재까지는 대략 이런 느낌입니다.


메인 콘텐츠

templates/homepage.hamlettemplates/homepage.cassius  각각을 아래와 같이 코드를 추가 작성하여 수정합니다.

templates/homepage.hamlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
<div #carousel-example-generic .carousel .slide data-ride="carousel">
  <ol .carousel-indicators>
    <li data-target="#carousel-example-generic" data-slide-to="0" .active></li>
    <li data-target="#carousel-example-generic" data-slide-to="1"></li>
    <li data-target="#carousel-example-generic" data-slide-to="2"></li>
  <div .carousel-inner>
    <div .item .active>
      <img src=@{StaticR images_images1_jpg} alt="slide-1">
      <div .carousel-caption>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit.
    <div .item>
      <img src=@{StaticR images_images2_jpg} alt="slide-2">
      <div .carousel-caption>
        A dolorem et ipsa doloremque tempore placeat voluptates repellat repudiandae autem? Eius,
    <div .item>
      <img src=@{StaticR images_images3_jpg} alt="slide-3">
      <div .carousel-caption>
        explicabo corporis eveniet ipsum velit labore quas voluptatum exercitationem fugit.
    <a .left .carousel-control href="#carousel-example-generic" data-slide="prev">
      <span .glyphicon .glyphicon-chevron-left>
    <a .right .carousel-control href="#carousel-example-generic" data-slide="next">
      <span .glyphicon .glyphicon-chevron-right>
 
<div #content>
  <div .container>
    <h2 .title>H2 TITLE HERE
    <div .row>
      <div .text-box .col-sm-6>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Consectetur, libero, quaerat, doloremque pariatur amet minima nihil enim temporibus doloribus dolorem neque sit quo id voluptas voluptate praesentium magnam? Unde, provident.sunt esse dolores dolorum deleniti asperiores commodi beatae ad veritatis voluptates dignissimos. Possimus!sunt esse dolores dolorum deleniti asperiores commodi beatae ad veritatis voluptates dignissimos. Possimus!
      <div .text-box .col-sm-6>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit. Fugit, iste, nesciunt, sapiente incidunt consequatur placeat voluptate sequi sunt esse dolores dolorum deleniti asperiores commodi beatae ad veritatis voluptates dignissimos. Possimus!sunt esse dolores dolorum deleniti asperiores commodi beatae ad veritatis voluptates dignissimos. Possimus!sunt esse dolores dolorum deleniti asperiores commodi beatae ad veritatis voluptates dignissimos. Possimus!
    <div .row>
      $forall _ <- [1,2,3]
        <div .col-sm-4>
          <div .panel .panel-default>
            <div .panel-body>
              Photo
            <div .panel-footer>
              CONTENTS
  <div .button-box>
    <p>CATCH TITLE
    <button type="button" .btn .btn-default>Button
cs



cassius 파일도 수정합니다. 완성된 모습입니다.

templates/homepage.cassius

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.navbar
  margin-bottom: 0
 
.carousel-inner img
  width: 100%
 
.title
  text-align: center
  margin-top: 40px
  margin-bottom: 20px
 
.text-box
  margin-bottom: 40px
 
.panel
  margin-bottom: 20px
  background-color: #D1D1D1
  border: none
  -webkit-border-radius: 0
  -moz-border-radius: 0
  border-radius: 0
 
.panel-body
  text-align: center
  height: 180px
  padding-top: 90px
  color: #888
  font-size: 18px
 
.panel-footer
  text-align: center
  padding: 18px 15px
  background-color: #7E7E7E
  border-top: none
  border-bottom-right-radius: 0
  border-bottom-left-radius: 0
  color: #fff
  font-size: 20px
  font-weight: bold
 
.button-box
  text-align: center
  padding: 60px 0
  margin-top: 40px
  background-color: #eee
 
.button-box p
  color: #888
 
.btn
  padding: 12px 80px
 
.button-box .btn-default
  color: #fff
  font-size: 18px
  font-weight: bold
  background-color: #7E7E7E
cs



이외의 구문

코멘트아웃

hamlet 의 경우 코멘트아웃을 위해 $# 을 이용합니다.

개행

hamlet 파일의 선두에 기술하는 것으로 파일의 개행을 제어한다.

구문의미
$newline always全ての行に改行を追加
$newline never改行無しで出力
$newline text連続したテキスト行にのみ改行を追加。“ ... ” の部分だけ改行が反映される


if 이외의 조건문

$if $elseif $else 는 이전 포스팅에 등장했지만, 그 이외에도 $maybe $nothing$case $of  등의 조건문이 존재한다.

maybe nothing

1
2
3
4
5
6
7
$maybe name <- maybeName
  <p>Your name is #{name}
$nothing
  <p>I don't know your name.
 
$maybe Person firstName lastName <- maybePerson
  <p>Your name is #{firstName} #{lastName} 
cs

case of

1
2
3
4
5
$case foo
  $of Left bar
    <p>It was left: #{bar}
  $of Right baz
    <p>It was right: #{baz}
cs

with

1
2
$with foo <- some ver (long ugly) expression that $ should only $ happen once
 <p>But I'm going to use #{foo} multiple times. #{foo}
cs

형안전 URL에 대해서

마지막으로 형안전 URL에 대해 간단히 설명합니다. 예를 들면, hamlet 에 아래의 코드가 기술되어 있는 경우

1
<a href=@{Time}>The time
cs


아래와 같이 전개됩니다.

1
\render -> mconcat ["<a href='", render Time, "'>The time</a>"]
cs


좀 더 자세한 설명

QuasiQuote 작성법을 이모저모 알아봅니다.

1
2
3
[shamlet|
<p>test
|]
cs


hamlet 에 관해서는 3종류 (엄밀히 말해서는 5종류) 가 준비되어 있으며, 표현되는 범위가 서로 상이합니다.

QuasiQuotes생성되는 식이 가지는 형변수전개형안전URL

국제화메시지

shamlet (simple)HtmlO--
hamletHtmlUrl urlOO-
ihamlet (International)HtmlUrlI18n msg urlOOO

생성되는 식은 각각 다른 형을 가지는 것이 포인트입니다.

shamlet 의 경우 구체적으로는 이런 식으로 전개됩니다. 

1
2
3
4
5
6
7
8
9
[shamlet|
<p>test
<p>test2
<p>test3
|]
 
-- 전개후
f :: Html
f = mconcat ["<p>test</p>","<p>test2</p>","<p>test3</p>"]
cs

다음으로 hamlet 의 경우입니다.

1
2
3
4
5
6
7
8
9
10
11
12
[hamlet|
<p>test
<a href=@{Link1R}>link1
<a href=@{Link2R}>link2
<p>test3
|]
 
-- 전개후
-- type HtmlUrl url = Render url -> Html
 
f :: HtmlUrl url
f render = mconcat ["<p>test</p>", "a href='", render Link1R, render Link2R, "'>link</a>", "<p>test3</p>"]
cs


렌더링용의 함수를 인수로 취급하여, 형안전URL을 문자열의 형식으로 변환합니다. shamlet 에는 형안전URL을 기술하는 것이 가능하지 않습니다. 여기서 어떤 렌더링함수가 전달되고 있나요? 라고 의문이 생긴다면, 실제로는 이것이 withUrlRenderer 의 역할입니다. 여기까지 이해가 되었다면 아래의 의미가 분명하지 않은 소스코드도 Yesod는 굉장히 생각하지 좋게 작성이 되는 것을 확인하실 수 있습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
myLayout :: Widget -> Handler Html
myLayout widget = do
  pc <- widgetToPageContent widget
  withUrlRenderer
    [hamlet|
      $doctype 5
      <html>
        <head>
          <title>#{pageTitle pc}
          ^{pageHead pc}
        <body>
          ^{pageBody pc}
    |]
cs


이번회에서는 Cassius와 Julius에 대해서는 완전히 소개하지는 못했지만, 어느 정도 copy and paste 만으로도 기존의 코드를 왔다갔다하는 것이 가능해져서, 학습은 그다지 어렵지 않았다고 생각합니다. 

[참고 링크]

https://www.yesodweb.com/book/scaffolding-and-the-site-template#scaffolding-and-the-site-template_static_files

https://stackoverflow.com/questions/44729207/yesod-static-file-type-safe-route-variable-not-in-scope