현재 공부중인 인강을 바탕으로 프로젝트 기본 구조는 정리
정리하고 나니, 전부는 아니지만 많은 부분에 대한 이해도가 높아짐
기본 구조는 아래와 같음.
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] 여기 페이지를 참고하세요.
사용한 외부 패키지는 아래와 같습니다.
-- 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 |
---|