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가지 방법이 있습니다.
config/routes
를 수정,GET
메서드만 남긴다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.hamlet
, templates/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) | Html | O | - | - |
hamlet | HtmlUrl url | O | O | - |
ihamlet (International) | HtmlUrlI18n msg url | O | O | O |
생성되는 식은 각각 다른 형을 가지는 것이 포인트입니다.
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
'프로그래밍 Programming' 카테고리의 다른 글
하스켈 패키지 삭제하기 How to uninstall a Haskell package installed with stack? (0) | 2018.02.06 |
---|---|
cabal install 사용방법 (1) How to cabal install (1) (0) | 2018.02.06 |
Haskell Yesod의 템플릿 언어 세익스피어를 활용하여 웹페이지 만들기 (1) (0) | 2018.02.01 |
Yesod의 위젯에 대해서 (1) (0) | 2018.01.27 |
하스켈 익스텐션 사용법 How to Enable Extensions (0) | 2018.01.26 |