본문 바로가기

GO lang/web Structure Sample

[GO] web Basic Structure

현재 공부중인 인강을 바탕으로 프로젝트 기본 구조는 정리

정리하고 나니, 전부는 아니지만 많은 부분에 대한 이해도가 높아짐

 

기본 구조는 아래와 같음.

 

 

 

main - 기본 설정 및 패키지 환경설정을 정의

middleware - CSRF 함수, Session 함수 정의

routes - 라우팅 연결, 웹주소별 html 연결 정의

 

 

 

 

config - 어플관련 환경 설정

 

 

 

errors - 화면입력시, 오류 메시지 저장 및 관리

forms - 폼 및 폼 데이터 검증, 관리

 

 

handlers - 실제 주소별 서버응답 정의.

 

 

models - 비지니스 모델의 데이터 구조 정의

templatedata - 화면 관련 데이터 구조 정의

 

 

render - tmpl 파일을 이용한 html 구현

 

 

 

 

styles - css 정의

 

 

 

basic.layout.tmpl - html 공통부

make-reservation.page.tmpl - 샘플 화면

 

 

 

 

 

 

 

 

여기서 모든 부분이 중요하지만 개인적으로이해하기 어려운 부분은 render.go / handlers.go 과 main.go 관계였다.

 

main.go

 

package main

import (
	"GO/Toy_Prj/basic_struct/internal/config"
	"GO/Toy_Prj/basic_struct/internal/handlers"
	"GO/Toy_Prj/basic_struct/internal/render"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/alexedwards/scs/v2"
)

const portNumber = ":3000"

var app config.AppConfig        // from config.go
var session *scs.SessionManager // from package

// main is the main function
func main() {

	// change this to true when in production, 보안강화 적용안함.
	app.InProduction = false

	// 세션관련 설정
	session = scs.New()
	session.Lifetime = 24 * time.Hour
	session.Cookie.Persist = true
	session.Cookie.SameSite = http.SameSiteLaxMode
	session.Cookie.Secure = app.InProduction // middleware.go > Secure 에서도 참조함
	app.Session = session

	tc, err := render.CreateTemplateCache() // tmpl 파일을 조립하여 메모리로 로딩
	if err != nil {
		log.Fatal("cannot create template cache")
	}

	app.TemplateCache = tc
	app.UseCache = false // (DEV mode)read cache everytime. 개발환경으로 tmpl 파일을 매번 갱신

	repo := handlers.NewRepo(&app) // main에서 선언한 AppConfig 변수를 handlers.go 와 공유
	// main에서 선언한 repo 객체를 handlers 에 전달하여 Repo 객체와 repo 객체의 메모리 매핑.
	// 다른 파일에서 Repo를 통해서 handlers 내부 함수에 접근 가능함.  사용예시 routes.go
	handlers.NewHandlers(repo)

	render.NewTemplates(&app) // main에서 선언한 AppConfig 변수를 render.go 와 공유

	tmp := fmt.Sprintf("Staring application on port %s", portNumber)
	fmt.Println(tmp)

	svr := &http.Server{
		Addr:    portNumber,
		Handler: routes(&app),
	}
	err = svr.ListenAndServe()
	log.Fatal(err)
}

 

이 부분이 기본 개념의 핵심부분이다.

 

 

middleware.go - CSRF 보안과 세션 관리 함수.

 

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/justinas/nosurf"
)

func WriteToConsole(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Println("Hit the page")
		next.ServeHTTP(w, r)
	})
}

// NoSurf andds CSRF protection to all POST requests
func NoSurf(next http.Handler) http.Handler {
	log.Println("call NoSurf")
	csrfHandler := nosurf.New(next)

	csrfHandler.SetBaseCookie(http.Cookie{
		HttpOnly: true,
		Path:     "/",
		Secure:   app.InProduction,
		SameSite: http.SameSiteDefaultMode,
	})

	return csrfHandler
}

// SessionLoad loads and saves the session on every requests
func SessionLoad(next http.Handler) http.Handler {
	log.Println("call SessionLoad")
	return session.LoadAndSave(next)
}

 

routes.go - 라이팅 패키지 할당 및 미들웨어 추가, chi는 routing 및 middware 기능을 지원한다

 

package main

import (
	"GO/Toy_Prj/basic_struct/internal/config"
	"GO/Toy_Prj/basic_struct/internal/handlers"
	"net/http"

	"github.com/go-chi/chi/v5"
	"github.com/go-chi/chi/v5/middleware"
)

func routes(app *config.AppConfig) http.Handler {

	mux := chi.NewRouter()
	mux.Use(middleware.Recoverer)
	mux.Use(NoSurf)
	mux.Use(SessionLoad)

	mux.Get("/make-reservation", handlers.Repo.Reservation)
	mux.Post("/make-reservation", handlers.Repo.PostReservation)

	fileServer := http.FileServer(http.Dir("./static/"))
	mux.Handle("/static/*", http.StripPrefix("/static", fileServer))

	return mux
}

 

handlers.go - 라우팅에서 매핑한 각각의 화면에 대응하는 서버쪽 처리 정의

 

package handlers

import (
	"GO/Toy_Prj/basic_struct/internal/config"
	"GO/Toy_Prj/basic_struct/internal/forms"
	"GO/Toy_Prj/basic_struct/internal/models"
	"GO/Toy_Prj/basic_struct/internal/render"
	"log"

	// _ "cycle"
	"net/http"
)

// Repo the repository used by the handlers
var Repo *Repository

// Repository is the repository type
type Repository struct {
	App *config.AppConfig
}

// NewRepo creates a new repository
func NewRepo(a *config.AppConfig) *Repository {
	return &Repository{
		App: a,
	}
}

/*
	NewHandlers sets the repository for the handlers
	Repo *Repository 선언을 main에서 하면 쉽게 사용가능하지만,
	다른 파일들에서는 사용하기 불편함. 이유는 다른파일에서 main 을 패키지로 처리하고 접근해야 함.
	Repo *Repository 선언을 핸들러 내부에서 하면 다른 파일에서 패키지 처리하고 접근하기 편함.
	반대로 errors.go, forms.go 에서는 자신의 구조체(errors, Form)를
	(Repo *Repository) 처럼 내부에서 선언을 하지 않음
	이유는 각 화면(view)당 각각 에러 및 필드 검증을 하기 위해서 임
*/
func NewHandlers(r *Repository) {
	Repo = r
}

// Reservation renders the make a reservation page and displays form
// 초기에 get 에 대한 응답시, 오류 메시지도 없고 데이터도 없는 상태임.
func (m *Repository) Reservation(w http.ResponseWriter, r *http.Request) {
	var emptyReservation models.Reservation
	data := make(map[string]interface{})
	data["reservation"] = emptyReservation

	// 응답 페이지에는 빈 데이터가 전달됨
	render.RenderTemplate(w, r, "make-reservation.page.tmpl", &models.TemplateData{
		Form: forms.New(nil),
		Data: data,
	})
}

// PostReservation handles the posting of a reservation form
// post 에 대한 응답시, 필드 데이터를 검증하여 오류시 오류 메시지 및 원래 데이터를 전달한다.
func (m *Repository) PostReservation(w http.ResponseWriter, r *http.Request) {
	err := r.ParseForm()
	if err != nil {
		log.Println(err)
		return
	}

	// post 로 전달된 데이터 저장, 아래에서 응답시 화면으로 전달함
	reservation := models.Reservation{
		FirstName: r.Form.Get("first_name"),
		LastName:  r.Form.Get("last_name"),
		Email:     r.Form.Get("email"),
		Phone:     r.Form.Get("phone"),
	}
	log.Println("reservation >>", reservation)

	// 각 필드데이터에 접근은 아래처럼 해도 된다.
	form := forms.New(r.PostForm)
	log.Println("handlers.go >>> forms.New:", form.Get("first_name"))

	// form.Has("first_name")
	form.Required("first_name", "last_name", "email")
	form.MinLength("first_name", 3, r)
	form.IsEmail("email")

	if !form.Valid() { // 해당 폼에 오류 메시지가 하나도 없어야 실행됨
		log.Println("!form.Valid()")
		data := make(map[string]interface{})
		data["reservation"] = reservation

		render.RenderTemplate(w, r, "make-reservation.page.tmpl", &models.TemplateData{
			Form: form,
			Data: data,
		})
		return
	}
}

 

render.go - 화면들을 모두 메모리에 로딩하여 호출시 지연을 최소화 함, CSRF 토근을 생성 및 저장

 

package render

import (
	"GO/Toy_Prj/basic_struct/internal/config"
	"GO/Toy_Prj/basic_struct/internal/models"
	"bytes"
	"fmt"
	"html/template"
	"log"
	"net/http"
	"path/filepath"

	"github.com/justinas/nosurf"
)

var functions = template.FuncMap{}

var app *config.AppConfig

// NewTemplates sets the config for the template package
// msin 에서 정의한 데이터 공유.
func NewTemplates(a *config.AppConfig) {
	app = a
}

func AddDefaultData(td *models.TemplateData, r *http.Request) *models.TemplateData {
	// <input type="hidden" name="csrf_token" value="{{.CSRFToken}}">
	// 아래 부분이 html 에서 불러오는 이름("{{.CSRFToken}}")과 같아야 함.
	td.CSRFToken = nosurf.Token(r)
	return td
}

// RenderTemplate renders a template
// TemplateData 를 이용하여 서버와 클라이언트 사이에 정보 공유함.
func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string, td *models.TemplateData) {
	var tc map[string]*template.Template

	if app.UseCache {
		// get the template cache from the app config
		// 운영시에는 true 로 변경하여 tmpl 화면을 메모리에서 호출하여 속도를 빠르게 함.
		tc = app.TemplateCache
	} else {
		// DEV mode 에서는 UseCache==false 이므로. read cache everytime.
		// 매번 화면 호출시마다 tmpl 파일을 계속 읽음. 수정시 서버 재시작 필요없음
		tc, _ = CreateTemplateCache()
	}

	// map에 원하는 페이지가 있는지 확인
	t, ok := tc[tmpl]
	if !ok {
		// return errors.New("Could not get template from template cache")
		log.Fatal("Could not get template from template cache")
	}

	buf := new(bytes.Buffer) // buf 생성
	td = AddDefaultData(td, r)

	_ = t.Execute(buf, td) // 해당 페이지를 buf 에 저장

	_, err := buf.WriteTo(w) // client 에게 전송
	if err != nil {
		// log.Println(err)
		fmt.Println("error writing template to browser", err)
		// return err
	}

}

// CreateTemplateCache creates a template cache as a map, page + base
func CreateTemplateCache() (map[string]*template.Template, error) {

	myCache := map[string]*template.Template{}

	// 폴더이름+파일명 저장
	pages, err := filepath.Glob("./templates/*.page.tmpl")
	if err != nil {
		return myCache, err
	}

	for _, page := range pages {
		// 폴더정보를 제거하고 파일 이름만 저장
		name := filepath.Base(page)

		// 페이지 정보 로딩
		ts, err := template.New(name).Funcs(functions).ParseFiles(page)
		if err != nil {
			return myCache, err
		}

		layouts, err := filepath.Glob("./templates/*.layout.tmpl")
		if err != nil {
			return myCache, err
		}

		if len(layouts) > 0 {
			// 페이지 정보에 base 정보를 추가 결함.
			ts, err = ts.ParseGlob("./templates/*.layout.tmpl")
			if err != nil {
				return myCache, err
			}
		}
		myCache[name] = ts

	}
	return myCache, nil
}

 

프로젝트 실행 방법(윈도우10 환경)

  • GO\Toy_Prj\basic_struct> go run ./cmd/web/.

 

샘플 페이지 테스트 결과 - 로딩화면

 

 

샘플 페이지 테스트 결과 - 오류화면

  • 검증 조건이 이름은 3글자 이상으로 설정되어 오류 발생한 경우
  • 공백인 경우 오류 발생한 경우
  • 이메일은 @ 및 . 조건이 충족해야 하므로 오류 발생한 경우

 

 

 

errors.go - 폼 데이터 오류 저장 및 관리

forms.go - 폼관련 데이터 검증

 

config.go - 어플리케이션 설정 관련 구조체 정의

models.go - 어플리케이션 데이터 구조체 정의

templatedata.go - 서버와 클라이언트간 데이터 구조체 정의

 

templates 폴더내의 tmpl 관련 처리는 [https://unsungit.tistory.com/104] 여기 페이지를 참고하세요.

 

[전체 소스코드 in Github]

 

 

사용한 외부 패키지는 아래와 같습니다.

 

-- session managing, github.com/alexedwards/scs
> go get github.com/alexedwards/scs/v2

-- middleware, github.com/justinas/nosurf
> go get github.com/justinas/nosurf

-- routing package chi, github.com/go-chi/chi
> go get -u github.com/go-chi/chi/v5

-- 필드별 다른 방식의 검증을 지원하는 패키지, 현재 이메일 검증용으로 사용함.
> go get github.com/asaskevich/govalidator

 

'GO lang > web Structure Sample' 카테고리의 다른 글

[GO] web Basic Structure 업글.  (0) 2021.12.24