반응형

프론트엔드 프레임워크 하면 Angular / React / Vue 그리고 최근 부각되고 있는 Svelte가 있는데요. 전문 개발자는 아니지만 조금은 손대본 경험을 토대로(Angular제외..) 특성을 알아보도록 하겠습니다.

 

Stackoverflow Ranking에서 여러 프로그래밍 언어 및 프레임워크에 대한 점유율, 선호도 등 자세한 정보는 확인하실 수 있습니다. 해당 정보에 따르면 웹 프레임워크의 점유율은 아래 그래프와 같습니다. 이 중에 프론트엔드 프레임워크만 보면 React가 제일 상단에 있고, jQuery, Angular, Vue 그리고 한참 아래쪽에 Svelte가 있네요

 

< 점유율 >

 

역시 Stackoverflow Ranking에 따르면 이번엔 Svelte가 제일 위에 있고, 그다음 React, Vue이고 Angular가 하위에 있는 모습입니다. Svelte가 가장 신생 프레임워크인데, 선호도 측면에서는 많은 개발자들이 만족하고 있다는 의미네요.

 

< 가장 사랑받는 / 피하고싶은>

 

그럼 각 프레임워크의 특징을 한번 살펴보겠습니다.

1. Angular

Angular는 구글에서 만든 Javascript 프레임워크로 초기 1.0버전에서는 AngularJS, 2.0버전부터는 그냥 Angular라고 칭했습니다. 특이한 것은 1.0은 Javascript, 2.0은 Typescript를 기본 언어로 채택하였습니다. Angular는 필요한 요소들을 모두 포함하고 있는 것이 특징입니다. 그만큼 다른 라이브러리들을 추가로 설치할 필요가 없으며, 대신 학습해야할 양이 많은 것으로 알려져 있습니다. 

양방향 데이터바인딩을 지원하며, 태그이름, 템플릿 파일, CSS파일 정보를 decorator문법을 이용하여 전달하는 선언적 코딩 스타일을 사용한다는 특징을 갖고 있습니다. 

 

 

2. React

React는 페이스북에서 만든 라이브러리입니다. 라이브러리냐 프레임워크냐로 갑론을박이 많이 있었지만, 최근에는 라이브러리다라는 의견이 대체적으로 받아들여지고 있는 모습입니다. React는 Virtual-DOM이라는 개념을 적용하였습니다. DOM은 HTML문서를 제어할 수 있는 API 트리 자료구조인데, 실제 DOM을 사용하여 변경시마다 모든 요소를 Update하기에는 무거우며 속도가 느려지는 한계가 있어 가상 DOM이라는 개념을 적용하고 있습니다. 이는 Vue도 마찬가지입니다. 

또다른 특징으로는 Component에 의한 재사용 가능한 UI 생성을 들 수 있으며, 이를 위해 내부적으로 JSX라는 특별한 문법을 사용합니다. 이는 자바스크립트에 HTML을 내포한듯한 모습을 하고 있습니다. 

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Hello React
        </a>
      </header>
    </div>
  );
}

Component로 각 요소를 구분해가며 전체 페이지를 구축해가는 방식은 개발자들이 쉽게 접근할 수 있도록 도왔던 것으로 보입니다. Props, State 등의 개념을 익히다가 나중에 상태관리자로 Redux라는 개념을 사용할 때즈음 되면 슬슬 멘붕이 오기 시작합니다. 요즘은 Redux toolkit를 써서 쉽게 접근할 수 있도록 했다고는 하지만, 개념이 바뀌는 것은 아니므로  학습곡선이 완전히 낮아졌다고 하기는 힘들어보입니다.

 

3. Vue

Evan You에 의해 시작된 Frontend Framework로 React와 마찬가지로 Virtual-DOM의 개념을 사용합니다. Evan You는 Angular를 이용한 개발을 하던 중 본인이 필요한 기능들만 추려서 만들었다고 하고, 시기적으로도 React보다 1년 뒤에 나온만큼 Angular, React 등으로부터 영향을 받았을 것으로 보입니다. 단방향 데이터 바인딩만 지원하는 React와는 달리 Vue는 양방향 데이터바인딩을 지원합니다. React를 공부하다보면 Redux 상태관리자를 사용하면서 학습난이도가 올라가는데, Vue에서의 상태관리자인 Vuex는 이보다 훨씬 이해하기 쉬운 장점이 있습니다. 아래는 컴포넌트를 정의하는 샘플 Vue 코드인데, 보시면 template, script, style의 세 부분으로 나뉘어져 있습니다. 이런 구분에 대해서는 개발자마다 호불호가 있지만, 처음 접근하는 입장에서 가독성이 좋은 것은 큰 장점 같습니다.

<template>
    <div class="btn">
        <slot></slot> <!--버튼처럼 텍스트를 표시하기위한 속성-->
    </div>
    <h1 @dblclick="$emit('abc')">
		ABC
    </h1>
</template>
    
<script>
export default{
	emits: [
		'abc'
	]    
}
</script>

<style scoped>
.btn{
    display: inline-block;
    margin: 4px;
    padding: 6px 12px;
    border-radius: 4px;
    background-color: gray;
    color: white;
    cursor: pointer;
}
</style>

 

4. Svelte

기존의 Angular/React/Vue에이어 Svelte는 상당히 나중에 진출한 후발주자입니다. 그런데도 Stackoverflow Ranking에서 보면 개발자가 사랑하는 프레레임워크 2위를 차지할 정도로 관심도가 엄청나며(1위는 Phoenix), 다른 프레임워크들을 추격하고 있습니다. 

The State of JS 2021 Web Frontend Framework

svelte 는 라이브러리/프레임워크가 아닌 컴파일러라고 합니다. Svelte코드를 실행시점에서 해석하지 않고 컴파일 단계에서 Vanilla Javascript 번들로 만들기 때문에 다른 라이브러리를 함께 배포할 필요가 없어집니다. 또다른 특징으로는 React나 Vue처럼  Virtual-DOM을 사용하지 않고 실제 DOM을 직접 제어합니다. 성능적으로 작은 번들사이즈와 DOM의 직접제어 덕분에 다른 프레임워크보다 빠른 반응속도를 보여줍니다. 

문법은 개인적인 느낌이 Vue랑 유사하게 생겼습니다. 가시성도 좋고 쉽다고 생각이 듭니다. 화면 Update할 시점을 인식시켜주기 위한 특이한 용법들도 있습니다만 다른 라이브러리들의 학습곡선에 비할 바가 아니라고 생각됩니다. 상태관리에서도 redux, vuex와 같이 외부 라이브러리를 설치할 필요가 없이 내부에 store를 기본으로 내장하고 있습니다. 

<script>
let name = "teo.yu"
</script>

<div>Hello world! {name}</div>

<style>
div { color: red }
</style>

<번들사이즈 비교>

파일사이즈가 작으면 다운로드속도가 올라가서 체감속도가 빨라집니다. 아래 결과를 보면 Svelte가 가장 작은 사이즈를 보이고 있습니다.

 

이런 성능과 러닝커브 등의 잇점으로 최근 svelte의 인기가 올라가고 있는 것으로 보입니다. 하지만 아직까지 시장의 메인은 React, 그리고 최근에는 Vue도 많이 도입하고 있는 것 같습니다. 저도 Vue를 주로 사용하여 개인 프로젝트들을 만들고 있는데 React에서 어려운 개념들을 익히고 나서 접근해서인지, Vue 너무 좋습니다. 성능도 준수하게 뽑아주는 것 같고, Reference도 잘 되어있어서 당분간은 Vue를 활용할 것 같습니다. Svelte는 조금 더 저변이 넓어지면 그때 적용해볼까 합니다. 

반응형
반응형
JSON / XML 응답

이 섹션에서는 요청 헤더에 따라 애플리케이션이 HTML, JSON 또는 XML 형식으로 응답 할 수 있도록 애플리케이션을 약간 리팩터링합니다.

1. 재사용 가능한 함수 만들기

Route Handler에서 지금까지는 Gin의 컨텍스트 중 HTML을 사용했습니다. 항상 HTML페이지를 보여줄 때는 괜찮지만, 요청에 따라 응답 형식을 변경하고 싶을 때에는 렌더링을 처리하는 단일 함수로 리팩토링해야합니다(?). 이렇게 함으로써 Route Handler는 유효성 검사(validation) 및 데이터 추출(data fetching)에 집중하도록 할 수 있습니다.

 

Route Handler는 응답 형식에 관계없이 동일한 유효성 검사, 데이터 추출 및 처리를 수행해야합니다. 이 부분이 완료되면 데이터를 사용하여 원하는 형식의 응답을 생성 할 수 있습니다. HTML 응답이 필요한 경우 데이터를 HTML 템플릿에 전달하고, JSON 응답이 필요한 경우이 데이터를 JSON으로 변환, XML에서도 마찬로 데이터를 보내줄 수 있습니다.

 

main.go 파일 내부에 모든 Route Handler가 사용할 render 함수를 만들겠습니다. 이 함수는 request의 Accept header를 참조하여 rendering을 처리할 것입니다.

 

Gin에서는 라우트 핸들러에 전달 된 Context 객체에 Request라는 필드가 포함됩니다. This field contains the header field which contains all the request headers. Get 메소드를 를 이용하여 Header로부터 Accept header를 다음과 같이 추출할 수 있습니다.

// c is the Gin Context 
c.Request.Header.Get("Accept")
  • application/json 으로 설정하면 함수가 JSON을 렌더링합니다.
  • application/xml 으로 설정하면 함수가 XML을 렌더링하고
  • 이것이 다른 것으로 설정되거나 비어 있으면 함수는 HTML을 렌더링합니다.

main.go에 render 함수를 아래와 같이 추가합니다.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

var router *gin.Engine

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/*")

	router.GET("/", showIndexPage)
	router.GET("/article/view/:article_id", getArticle)

	router.Run()

}

func render(c *gin.Context, data gin.H, templateName string) {

	switch c.Request.Header.Get("Accept") {
	case "application/json":
		// Respond with JSON
		c.JSON(http.StatusOK, data["payload"])
	case "application/xml":
		// Respond with XML
		c.XML(http.StatusOK, data["payload"])
	default:
		// Respond with HTML
		c.HTML(http.StatusOK, templateName, data)
	}

}

2. 유닛 테스트를 통한 Route Handler 요구사항 정의 =>생략...

3. Route Handler 업데이트

기존의 c.HTML방법을 render 함수로 바꿔주기만 하면 됩니다.

 

<기존>

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

<변경>

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the render function with the name of the template to render
  render(c, gin.H{
    "title":   "Home Page",
    "payload": articles}, "index.html")

}

 

JSON 형식으로 Article List 추출

JSON테스트를 위해새로 어플리케이션을 빌드한 후 아래를 실행합니다.

curl -X GET -H "Accept: application/json" http://localhost:8080/

다음과 같은 응답을 반환해야합니다.

[{"id":1,"title":"Article 1","content":"Article 1 body"},{"id":2,"title":"Article 2","content":"Article 2 body"}]

 

XML 형식으로 Article 추출

XML 테스트를 위해 아래의 명령어를 실행합니다.

curl -X GET -H "Accept: application/xml" http://localhost:8080/article/view/1

다음과 같은 응답을 반환해야합니다.

<article><ID>1</ID><Title>Article 1</Title><Content>Article 1 body</Content></article>

 

4. Application 테스트

 이제 우리가 작성한 테스트를 실행하고 결과를 확인해 보겠습니다. 프로젝트 폴더에서 다음 명령을 실행합니다.

go test -v

이 명령을 실행하면 아래와 유사한 결과가 나타납니다.

=== RUN   TestShowIndexPageUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 |     183.315µs |  |   GET     /
--- PASS: TestShowIndexPageUnauthenticated (0.00s)
=== RUN   TestArticleUnauthenticated
[GIN] 2016/06/14 - 19:07:26 | 200 |     143.789µs |  |   GET     /article/view/1
--- PASS: TestArticleUnauthenticated (0.00s)
=== RUN   TestArticleListJSON
[GIN] 2016/06/14 - 19:07:26 | 200 |      51.087µs |  |   GET     /
--- PASS: TestArticleListJSON (0.00s)
=== RUN   TestArticleXML
[GIN] 2016/06/14 - 19:07:26 | 200 |      38.656µs |  |   GET     /article/view/1
--- PASS: TestArticleXML (0.00s)
=== RUN   TestGetAllArticles
--- PASS: TestGetAllArticles (0.00s)
=== RUN   TestGetArticleByID
--- PASS: TestGetArticleByID (0.00s)
PASS
ok    github.com/demo-apps/go-gin-app 0.084s

이 출력에서 ​​볼 수 있듯이이 명령은 우리가 작성한 모든 테스트를 한번에 실행하며,이 경우 애플리케이션이 의도 한대로 작동하고 있음을 나타냅니다. 출력을 자세히 살펴보면 Go가 경로 핸들러를 테스트하는 과정에서 HTTP 요청을 수행했음을 알 수 있습니다.

 


이렇게 Gin Framework에 대한 강좌 따라하기를 수행해보았는데요, 마지막 JSON/XML 부분과 최종 테스트 부분은 아직 정확히 테스트는 못해보고 그냥 자료 번역 수준으로 올려놓습니다. 나중에라도 익숙해지면 해보려구요. 혹시라도 참고가 되시면 좋겠습니다.

 

그럼 이만~~~

반응형
반응형
개별 Article 화면 구성

현재까지 구성한 앱에서는 Article을 클릭했을 때 링크가 작동하지 않았습니다. 이번에는 각 Article에 대한 핸들러와 템플릿을 추가하도록 하겠습니다.

1. Route 설정

router.GET("/article/view/:article_id", getArticle)

main함수의 Route 설정 부분에 위구문을 추가합니다. 위 라우터는 패턴과 맞는 요청에 대해 경로를 일치시키고, 경로 마지막부분을 route 파라미터인 article_id에 저장합니다. 이 라우터에서는 handler함수로 getArticle을 정의합니다.

 

main.go 전체 코드 모습

package main

import (
	//"net/http"

	"github.com/gin-gonic/gin"
)

var router *gin.Engine

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/*")

	router.GET("/", showIndexPage)
	router.GET("/article/view/:article_id", getArticle)

	router.Run()

}

 

2. View Templates 만들기

개별 Article Contents를 표현할 새 템플릿 templates/article.html 을 생성하겠습니다.

<!--article.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

<!--Display the title of the article-->
<h1>{{.payload.Title}}</h1>

<!--Display the content of the article-->
<p>{{.payload.Content}}</p>

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

유닛 테스트....가 필요한데 Pass...

3. Route Handler 생성

Article 추출

models.article.go파일에  getArticleByID() 함수 정의하여 article을 가져오는 기능을 추가합니다.

package main

import "errors"

type article struct {
	ID      int    `json:"id"`
	Title   string `json:"title"`
	Content string `json:"content"`
}

// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
	article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
	article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

// Return a list of all the articles
func getAllArticles() []article {
	return articleList
}

func getArticleByID(id int) (*article, error) {
	for _, a := range articleList {
		if a.ID == id {
			return &a, nil
		}
	}
	return nil, errors.New("Article not found")
}

getArticleByID 함수는 Article List를 반복하다가 ID가 일치할 경우 해당 Article을 반환합니다. 일치하는 기사가 없으면 오류를 반환합니다.

 

handlers.article.go파일 수정

Article을 article.html 템플릿에 전달하여 렌더링하도록 getArticle 함수를 추가합니다.

// handlers.article.go

package main

import (
  "net/http"
  "strconv"

  "github.com/gin-gonic/gin"
)

func showIndexPage(c *gin.Context) {
  articles := getAllArticles()

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

func getArticle(c *gin.Context) {
  // Check if the article ID is valid
  if articleID, err := strconv.Atoi(c.Param("article_id")); err == nil {
    // Check if the article exists
    if article, err := getArticleByID(articleID); err == nil {
      // Call the HTML method of the Context to render a template
      c.HTML(
        // Set the HTTP status to 200 (OK)
        http.StatusOK,
        // Use the index.html template
        "article.html",
        // Pass the data that the page uses
        gin.H{
          "title":   article.Title,
          "payload": article,
        },
      )

    } else {
      // If the article is not found, abort with an error
      c.AbortWithError(http.StatusNotFound, err)
    }

  } else {
    // If an invalid article ID is specified in the URL, abort with an error
    c.AbortWithStatus(http.StatusNotFound)
  }
}

 

<결과>

 

 

 

<파일구성>

 

 

 

 
반응형
반응형

Article List 표시

이 섹션에서는 index 페이지에 모든 article list를 표시하는 기능을 추가합니다.

1.Router 설정

원문에 따르면 응용 프로그램이 커질 것을 대비하여 별도의 Router파일에서 경로를 정의하는 방식으로 구성하였는데, 무슨 문제인지 제 실습 중에는 routes.go파일에 따로 코드를 분리하니 에러가 발생했습니다. 그래서 route 를 main() 함수 내부에 구성하도록 하겠습니다. 단, route handler 함수만 별도로 분리해 내도록 하겠습니다.(handlers.article.go)

 

main.go파일은 아래와 같이 코딩합니다.

package main

import (
	//"net/http"

	"github.com/gin-gonic/gin"
)

var router *gin.Engine

func main() {
	router := gin.Default()

	router.LoadHTMLGlob("templates/*")
	
	router.GET("/", showIndexPage)

	router.Run()

}

2. Article Model 디자인

router를 마치기 전에 우선 Model을 구성하도록 합니다. 우리가 구성할 Article 모델은 Id, Title, Content의 세 개의 필드 만 사용하여 구성합니다. 일반적인 어플리케이션이라면 당연히 데이터베이스를 사용하지만, 예제에서는 이를 단순화하기 위해 아래와 같이 하드코딩된 List변수를 사용하도록 하겠습니다. 그리고 모든 Article List을 반환하는 함수가 필요합니다. 이 함수의 이름을 getAllArticles()로 하고 models.article.go 파일에 함께 구성합니다.

 

models.article.go

// models.article.go

package main

type article struct {
  ID      int    json:"id"
  Title   string json:"title"
  Content string json:"content"
}

// For this demo, we're storing the article list in memory
// In a real application, this list will most likely be fetched
// from a database or from static files
var articleList = []article{
  article{ID: 1, Title: "Article 1", Content: "Article 1 body"},
  article{ID: 2, Title: "Article 2", Content: "Article 2 body"},
}

// Return a list of all the articles
func getAllArticles() []article {
  return articleList
}

또한 단위 테스트를 위한 models.article_test.go 파일을 생성하고 내부에 TestGetAllArticles 함수를 작성하겠습니다.

 

models.article_test.go

// models.article_test.go

package main

import "testing"

// Test the function that fetches all articles
func TestGetAllArticles(t *testing.T) {
  alist := getAllArticles()

  // Check that the length of the list of articles returned is the
  // same as the length of the global variable holding the list
  if len(alist) != len(articleList) {
    t.Fail()
  }

  // Check that each member is identical
  for i, v := range alist {
    if v.Content != articleList[i].Content ||
      v.ID != articleList[i].ID ||
      v.Title != articleList[i].Title {

      t.Fail()
      break
    }
  }
}

위 단위 테스트에서는 getAllArticles() 함수를 이용해 모든 Article List를 가져오는 테스트 수행합니다. 이 테스트는 먼저getAllArticles() 함수로 가져온 Article List와 전역 변수 articleList의 요소 개수가 동일한지 확인합니다. 그런 다음 Article List들을 반복하여 각각의 Article이 동일한 지 확인합니다. 이 두 검사 중 하나가 실패하면 테스트는 Fail이 됩니다.

3. View Template 만들기

index.html에서 모든 Article들의 List를 표시해야합니다. Article List는 payload라는 이름의 변수로 전달하도록 하겠습니다. 아래는 모든 article list를 보여주게 됩니다. 모든 Article List에 대해 Title, Content를 표시해주게 됩니다만, 아직 라우터를 완성하지 않았으므로 표시되는 건 없습니다.

 

 

업데이트 된 index.html파일은 아래와 같습니다.

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <!--Loop over the payload variable, which is the list of articles-->
  {{range .payload }}
    <!--Create the link for the article based on its ID-->
    <a href="/article/view/{{.ID}}">
      <!--Display the title of the article -->
      <h2>{{.Title}}</h2>
    </a>
    <!--Display the content of the article-->
    <p>{{.Content}}</p>
  {{end}}

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

(주의) header에는 웹페이지의 타이틀을 표시하는 {{.title}}변수가 있고, index에서 호출하는 {{.Title}}변수는 Article의 타이틀을 의미합니다.

4. 유닛 테스트를 통한 Route Handler 요구사항 정의

Index 라우트 생성 전에 이 route handler의 동작을 미리 예상한 테스트를 정의하겠습니다. 이 테스트에서 확인할 조건은 다음과 같습니다.

  • 핸들러는 HTTP 상태 코드 200으로 응답한다.
  • 반환 된 HTML에는 "Home Page"라는 텍스트가 포함 된 title tag가 포함된다.

 

handlers.article_test.go파일을 생성하고, 내부에 TestShowIndexPageUnauthenticated 라는 테스트 함수를 생성합니다. 그리고 이 함수에서 사용할 helper 함수를 common_test.go파일에 작성합니다.

 

handlers.article_test.go다음과 같습니다.

// handlers.article_test.go

package main

import (
  "io/ioutil"
  "net/http"
  "net/http/httptest"
  "strings"
  "testing"
)

// Test that a GET request to the home page returns the home page with
// the HTTP code 200 for an unauthenticated user
func TestShowIndexPageUnauthenticated(t *testing.T) {
  r := getRouter(true)

  r.GET("/", showIndexPage)

  // Create a request to send to the above route
  req, _ := http.NewRequest("GET", "/", nil)

  testHTTPResponse(t, r, req, func(w *httptest.ResponseRecorder) bool {
    // Test that the http status code is 200
    statusOK := w.Code == http.StatusOK

    // Test that the page title is "Home Page"
    // You can carry out a lot more detailed tests using libraries that can
    // parse and process HTML pages
    p, err := ioutil.ReadAll(w.Body)
    pageOK := err == nil && strings.Index(string(p), "<title>Home Page</title>") > 0

    return statusOK && pageOK
  })
}

 

common_test.go다음과 같습니다.

package main

import (
  "net/http"
  "net/http/httptest"
  "os"
  "testing"

  "github.com/gin-gonic/gin"
)

var tmpArticleList []article

// This function is used for setup before executing the test functions
func TestMain(m *testing.M) {
  //Set Gin to Test Mode
  gin.SetMode(gin.TestMode)

  // Run the other tests
  os.Exit(m.Run())
}

// Helper function to create a router during testing
func getRouter(withTemplates bool) *gin.Engine {
  r := gin.Default()
  if withTemplates {
    r.LoadHTMLGlob("templates/*")
  }
  return r
}

// Helper function to process a request and test its response
func testHTTPResponse(t *testing.T, r *gin.Engine, req *http.Request, f func(w *httptest.ResponseRecorder) bool) {

  // Create a response recorder
  w := httptest.NewRecorder()

  // Create the service and process the above request.
  r.ServeHTTP(w, req)

  if !f(w) {
    t.Fail()
  }
}

// This function is used to store the main lists into the temporary one
// for testing
func saveLists() {
  tmpArticleList = articleList
}

// This function is used to restore the main lists from the temporary one
func restoreLists() {
  articleList = tmpArticleList
}

 

테스트를 도와주는 helper 함수 몇개를 작성했습니다. 이 함수들은 유사한 기능 테스트를 위한 추가 테스트 코드를 작성할 때 상용구 코드를 줄이는 데에도 도움이됩니다.

 

TestMain함수는 Gin이 테스트 모드를 사용하도록 설정하고 나머지 테스트 함수를 호출합니다. getRouter함수는 기본 애플리케이션과 유사한 방식으로 라우터를 생성/반환합니다. saveLists() 함수는 (original) Article List를 임시 변수에 저장합니다. 임시 변수는 restoreLists() 함수에서 사용되어,  단위 테스트가 실행 된 후 Article List를 테스트 수행 전 초기 상태로 복원하는 역할을 합니다.

 

마지막으로 testHTTPResponse함수는 인자로 전달 된 함수를 실행하여 true/false를 반환하는지 확인합니다.(테스트 성공/실패). 이 함수는 HTTP 요청에 대한 응답을 테스트하는 데 필요한 중복 코드를 방지해줍니다.

 

HTTP 코드와 반환 된 HTML을 확인하기 위해 다음을 수행합니다.

  • 새 router 생성.
  • 메인 앱에서 사용하는 것과 동일한 handler를 사용하는 route 정의( showIndexPage),
  • 이 route에 접근하기 위한 새 request 생성
  • HTTP 코드와 HTML을 테스트하기 위한 응답처리 함수 생성
  • testHTTPResponse() 호출. 단, 새로 생성한 응답처리 함수와 연결.

 

5. Route Handler 생성

handlers.article.go파일에 Article 관련 기능들에 대한 모든 route handler 함수인 showIndexPage를 정의합니다. payload라는 변수를 통해 Article List를 템플릿에 전달하도록 수정하였습니다.

 

handlers.article.go파일

// handlers.article.go

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func showIndexPage(c *gin.Context) {
  articles := getAllArticles() // 기존에 정의한 getAllArticles 함수를 이용하여 article list 가져오기

  // Call the HTML method of the Context to render a template
  c.HTML(
    // Set the HTTP status to 200 (OK)
    http.StatusOK,
    // Use the index.html template
    "index.html",
    // Pass the data that the page uses
    gin.H{
      "title":   "Home Page",
      "payload": articles,
    },
  )

}

<실행>

 

 

<앱 구성>

 

 

 

 
 
 
 
 

 

 
 
 
 
 
반응형
반응형

이것저것 하다보니 일관된 포스팅 하기가 힘드네요...^^;; 오늘은 Gin Framework 강좌를 따라해볼까 합니다. 원 Tutorial은 여기를 참고했습니다. 파일 구성은 아래와 같이 구성하는 것으로 시작합니다.

 

 

1. main.go

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	r.LoadHTMLGlob("templates/*")

	r.GET("/", func(c *gin.Context) {		
        // OK 이면 index.html파일에 JSON데이터를 넘겨서 보여줌 
		c.HTML(http.StatusOK, "index.html", gin.H{
				"title": "Home Page",
			},
		)
	})

	r.Run()

}

 

2. index.html

<!--index.html-->

<!--Embed the header.html template at this location-->
{{ template "header.html" .}}

  <h1>Hello Gin!</h1>

<!--Embed the footer.html template at this location-->
{{ template "footer.html" .}}

3. header.html

<!--header.html-->

<!doctype html>
<html>

  <head>
    <!--Use the title variable to set the title of the page-->
    <title>{{ .title }}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="UTF-8">

    <!--Use bootstrap to make the application look nice-->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous">
    <script async src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
  </head>

  <body class="container">
    <!--Embed the menu.html template at this location-->
    {{ template "menu.html" . }}

 

4. footer.html

<!--footer.html-->

  </body>

</html>

5. menu.html

<!--menu.html-->

<nav class="navbar navbar-default">
  <div class="container">
    <div class="navbar-header">
      <a class="navbar-brand" href="/">
        Home
      </a>
    </div>
  </div>
</nav>

 

<결과>

 

 

 

 

반응형
반응형
package main

import (
	"fmt"
	"image/color"
	_ "unicode/utf8"

	"fyne.io/fyne"
	"fyne.io/fyne/app"
	"fyne.io/fyne/canvas"
	"fyne.io/fyne/layout"
	"fyne.io/fyne/theme"
	"fyne.io/fyne/widget"
)

func main() {
	f := app.New()
	//f.Settings().SetTheme(theme.LightTheme())
	f.Settings().SetTheme(theme.DarkTheme())
	w := f.NewWindow("")

	//상단에 넣을 위젯 및 레이아웃 - NewFormLayout
	qry := widget.NewEntry()
	btn_go := widget.NewButton("New Memo", New_btn)

	text := canvas.NewText("Text Object", color.White)
	text.Alignment = fyne.TextAlignTrailing
	text.TextStyle = fyne.TextStyle{Italic: true}

	ret := fyne.NewContainerWithLayout(layout.NewFormLayout())
	ret.AddObject(btn_go)
	ret.AddObject(qry)

	//하단에 넣을 위젯 및 전체 레이아웃 구성 - NewBorderLayout
	label2 := widget.NewLabel("Simple Memo")
	labox2 := fyne.NewContainerWithLayout(layout.NewCenterLayout(), label2)

	b1 := widget.NewButton("Go1", func() { fmt.Println("Go1 Button") })
	b1.ExtendBaseWidget(b1)

	b2 := widget.NewButton("Go2", func() { fmt.Println("Go2 Button") })
	b2.ExtendBaseWidget(b2)

	out_entry := widget.NewMultiLineEntry()
	out_entry.SetPlaceHolder("결과...")
	out_entry.ExtendBaseWidget(out_entry)

	frm := fyne.NewContainerWithLayout(layout.NewBorderLayout(ret, labox2, nil, nil)) //상, 하, 좌(없음), 우(없음)
	frm.AddObject(ret)                                                                //상단
	frm.AddObject(labox2)                                                             //하단
	frm.AddObject(out_entry)                                                          //좌-우가 없으므로 5번째에(center) 추가됨

	w.SetContent(frm)
	w.Resize(fyne.Size{Height: 640, Width: 480})
	w.ShowAndRun()

}

 

반응형

+ Recent posts