본문 바로가기
Golang/Tucker

[묘공단] 7주차 (1)

by 윤원용 2023. 11. 17.

이 글은 골든래빗 《Tucker의 Go 언어 프로그래밍》의 31장 써머리입니다.

 

마지막인 31장은 Todo List 기능을 제공하는 웹 애플리케이션을 만들어야 하기 때문에 B/E(Back-End)뿐만 아니라 F/E(Front-End) 관련 내용도 포함된다. 본인은 책의 내용을 기반으로 재구성해서 리뷰할 예정이고 B/E는 Golang을 F/E는 react를 사용할 예정이다. 참고로 책에는 heroku(헤로쿠)를 사용해 배포까지 알려주고 있으니 책을 사서 보면 많은 경험을 할 수 있을 것이다.

 

더보기
  • 31. Todo 리스트 웹 사이트 만들기
    • 31.1 해법
    • 31.2 준비하기
    • 31.3 웹 서버 만들기
    • 31.4 프론트엔드 만들기
    • 31.5 웹 배포 방법 고려하기
    • 31.6 헤로쿠로 배포하기

 

시작하기 앞서 책에서 사용하는 외부 라이브러리들을 공유한 뒤 본인이 만드는 예제의 사항을 공유하겠다.

31. Todo 리스트 웹 사이트 만들기

책에는 31.1 해법에 어떻게 코딩해야 하는지 가이드를 제공할 뿐이고 Todo List가 무엇인지 알려주지 않는다. Todo List를 간단하게 설명하면 앞으로 할 일들을 나열하는 것이다. 그 외 추가적인 기능들을 어떻게 붙이냐는 논외지만 다양하게 해석할 수 있다.

 

B/E부터 시작할 예정인데 본인 회사에서도 Golang을 사용하며, 이번에는 처음부터 내 맘대로 만들 예정이다. 회사에선 어느 정도 만든 코드를 유지보수하는 방식으로 코딩하는 환경이라 처음부터 만들어 보고 싶었는데 좋은 기회가 온 것 같다. 때문에 실험적인 코드들도 많을 것으로 예상되며, 엉망일 수도 있을 것 같다. ㅎㅎ;; 

 

처음으로 해야 할 것은 echov4 라이브러리로 웹 서버의 기반을 구축한다.

go get github.com/labstack/echo/v4

라이브러리를 받았으면 main.go에 서버를 시작할 수 있도록 코딩한다.

package main

import (
	core_server "gt/chap31/ex/core/server"
)

func main() {
	if err := core_server.EchoServerStart(":3250"); err != nil {
		panic(err)
	}
}

추후 레디스도 초기화해야 하기 때문에 코드를 분리했다.

다음은 core_server 코드다.

package core_server

import (
	core_router "gt/chap31/ex/core/router"

	"github.com/labstack/echo/v4"
	echoMiddleware "github.com/labstack/echo/v4/middleware"
)

func EchoServerStart(address string) error {
	echoServer := echo.New()
	echoServer.Use(getCorsMiddleware())
	routerLoad(echoServer)
	return echoServer.Start(address)
}

func getCorsMiddleware() echo.MiddlewareFunc {
	return echoMiddleware.CORSWithConfig(getCorsConfig())
}

func getCorsConfig() echoMiddleware.CORSConfig {
	return echoMiddleware.CORSConfig{
		AllowOrigins:     []string{"*"},
		AllowMethods:     []string{echo.GET, echo.PUT, echo.POST, echo.DELETE, echo.OPTIONS},
		AllowHeaders:     []string{echo.HeaderAccept, echo.HeaderContentType},
		AllowCredentials: true,
		MaxAge:           60 * 10,
	}
}

func routerLoad(e *echo.Echo) {
	core_router.Load(e)
}

F/E에서 B/E로 호출하는 과정에 CORS가 발생했었기 때문에 CORS를 해결하기 위해 미들웨어를 추가했고 routerLoad 함수는 handler를 grouping 하기 위해 따로 뺐다.

다음은 core_router 코드다.

package core_router

import (
	"fmt"
	todo_router "gt/chap31/ex/todo/router"
	"net/http"

	"github.com/labstack/echo/v4"
)

func Load(e *echo.Echo) {
	e.Pre(pre())
	todoGroup := e.Group("/todos")
	todo_router.Load(todoGroup)
}

func pre() echo.MiddlewareFunc {
	return func(next echo.HandlerFunc) echo.HandlerFunc {
		return func(c echo.Context) error {
			header := c.Request().Header
			if header.Get("Content-Type") != "application/json" {
				header.Set("Content-Type", "application/json")
				c.Request().Header = header
			}

			err := next(c)

			if err != nil {
				fmt.Println(err.Error())
				c.JSON(http.StatusInternalServerError, map[string]interface{}{
					"err": err.Error(),
				})
			}

			header = c.Response().Header()
			if header.Get("Content-Type") != "application/json" {
				c.Response().Header().Set("Content-Type", "application/json")
			}
			return nil
		}
	}
}

pre 함수에서 request와 response의 기본 Content-Type를 설정하고 Exception이 발상했을 때 이유를 출력하고 공통으로 반환하도록 만들었다. 이 부분은 Error를 별도로 만들어서 리팩토링을 하면 좋을 것 같다. 

 

다음은 todo_router 코드를 공유하기 전에 todo 폴더 구조를 보면 좋을 것 같다.

구조를 간단하게 설명하면 dao는 data access object의 줄임말인데 repository랑 비슷하다 생각하면 될 것 같고 handler는 router에서 path와 httpMethod를 mapping 할 함수들을 구현한 파일이다. model 같은 경우는 F/E과 데이터를 주고받을 때 사용하는 dto(data transfer object)와 todo 데이터를 구성하는 struct가 있는 entity 같은 todo 파일로 구성했다.

package todo_router

import (
	todo_handler "gt/chap31/ex/todo/handler"

	"github.com/labstack/echo/v4"
)

func Load(e *echo.Group) {
	e.GET("/:writer", todo_handler.FindAllHandler)
	e.POST("", todo_handler.RegistHandler)
	e.PUT("/:writer/:id", todo_handler.ModifyHandler)
	e.PATCH("/:writer/:id", todo_handler.ClearFlagToggleHandler)
	e.DELETE("/:writer/:id", todo_handler.DestroyHandler)
	e.DELETE("/:writer", todo_handler.DestroyAllHandler)
}

handler 코드는 조금 길어서 함수별로 간단하게 설명하는 정도로 마무리하겠다.

FindAllHandler는 writer 기준으로 redis에 저장한 Todo 목록을 반환하며, RegistHandler는 Todo를 저장하는 handler이다.
RegistHandler 같은 경우는 writer를 별도로 path에 추가하지 않았는데 이유는 body로 받을 생각이었다. 그냥 path로 받아도 좋을 것 같다. ModifyHandler는 만들어 놓기만 하고 구현하진 않았다. ㅎㅎ;; 원래 목적은 Todo를 수정하기 위한 api였다. ClearFlagToggleHandler는 writer와 Todo의 id로 Todo 달성 여부를 갱신할 수 있는 api고 DestroyHandler도 writer와 Todo의 id로 Todo를 삭제하며, DestroyAllHandler는 writer의 모든 Todo를 삭제하도록 수 있도록 api를 구성했다. 사실 DestroyAllHandler 같은 경우는 테스트하기 위해 만들었고 F/E에 기능 자체를 만들진 않았다. B/E를 구현하면서 제일 아쉬웠던 점은 추후에 redis를 RDB로 변경할 때 유연하게 data를 관리하는 api의 반환 형식을 []byte로 했는데 이 부분이 제일 아쉽고 후회하는 부분이었다.

 

redis 같은 경우도 너무 길어서 넘어갈 건데 제일 리팩터링 하고 싶은 부분이다. 이 글을 보는 당신의 시간이 괜찮을 때 github에 가서 리뷰해 주면 감사할 것 같다.

 

F/E도 마찬가지로 실험적인 부분이 많은 코드다. 약간 잡담을 하자면 회사에서 사용하기 전에 궁금해서 스터디했었는데 얼떨결에 회사에서도 코딩하고 있다... B/E는 Golang, F/E는 react-typescript로 업무에서 사용하고 있는데 B/E 사수님과 F/E 사수님이 다르고 코딩 스타일과 추구하는 사상도 많이 다른 것 같다. F/E 사수님이 코딩한 것을 보면서 든 생각은 분리를 잘한 모듈을 사용하는 느낌을 많이 받았다. 이번에도 그렇게 따라 해 볼까? 생각했지만 실력이... 없어서 못 할 것 같아 그냥 만들어 봤다. ㅎㅎ;; 참고로 본인의 게으름으로 인해 시간에 쫓겨 맘에 안 드는 코드를 만들었다 ㅠㅠ;;

 

일단 결과를 보면

Todo List 정렬.... 미구현 ㅋ

CSS 허접이라 넘 이상하다 ㅋㅋㅋ 하지만 같이 스터디하는 조이님이 많이 알려주셔서 저 정도지 혼자 했으면 망했을 것 같다 ㅎㅎ ㅋㅋ

import { FC, useContext, useState, useEffect, useCallback, useRef, KeyboardEvent } from "react";
import { v4 as uuidV4 } from "uuid";
import TodoWrap from "./Wrap";
import "./index.css";
import ServerContext, { HttpMethods, ServerCallParamType } from "../server/ServerContext";

type TodoListType = {
    info?: Record<string, string>,
    keys?: string[],
    values?: string[]
};

type TodoType = {
    id: string,
    content: string,
    check: boolean,
};

const TodoList = () => {
    const [writer, setWriter] = useState<string | null>(null);
    const [refresh, setRefresh] = useState<boolean>(false);
    const [list, setList] = useState<TodoListType>({
        
    });
    const server = useContext(ServerContext);
    
    useEffect(() => {
        const storage = window.localStorage;
        let writer = storage.getItem("writer");
        if (writer === null) {
            writer = uuidV4();
        }
        storage.setItem("writer", writer);
        setWriter(writer);
    }, []);

    useEffect(() => {
        if (writer === null) {
            return;
        }
        const getList = async () => {
            const parmas: ServerCallParamType = {
                method: HttpMethods.GET,
                path: `todos/${writer}`
            };
            const data = await server.api<TodoListType>(parmas);
            setList(data);
        }

        getList();
    }, [writer, server, refresh]);

    const insert = useCallback(async (content: string) => {
        try {
            const parmas: ServerCallParamType = {
                method: HttpMethods.POST,
                path: `todos`,
                body: {
                    writer,
                    content
                }
            };
            const data = await server.api<string>(parmas);
            if (data === "ok") {
                setRefresh((refresh) => !refresh);
            }
        } catch(e) {
            console.error(e);
        }
    }, [writer, server]);

    const check = useCallback(async (id: string) => {
        try {
            const parmas: ServerCallParamType = {
                method: HttpMethods.PATCH,
                path: `todos/${writer}/${id}`
            };
            const data = await server.api<string>(parmas);
            if (data === "ok") {
                setRefresh((refresh) => !refresh);
            }
        } catch(e) {
            throw e;
        }
    }, [writer, server]);

    const remove = useCallback(async (id: string) => {
        try {
            
            const parmas: ServerCallParamType = {
                method: HttpMethods.DELETE,
                path: `todos/${writer}/${id}`
            };
            const data = await server.api<string>(parmas);
            if (data === "ok") {
                setRefresh((refresh) => !refresh);
            }
        } catch(e) {
            throw e;
        }
    }, [writer, server]);

    return (
        <TodoWrap>
             <section className="todo_list">
                <TodoInput 
                    insertFn={ insert }
                />
                {
                    list.values &&
                        list.values.map((str: string) => {
                        const todo = JSON.parse(str) as TodoType
                        return (
                            <TodoCard 
                                key={todo.id}
                                info={todo}
                                checkFn={ check }
                                removeFn={ remove }
                            />
                        )
                    })
                }
            </section>
        </TodoWrap>
    );
};

const TodoInput: FC<{insertFn: (content: string) => Promise<void> }> = ({ insertFn }) => {
    const inputRef = useRef<HTMLInputElement>(null);
    const insert = async () => {
        const { current } = inputRef;
        if (current === null) {
            throw new Error("current not found!!!.");
        }
        const { value } = current;
        await insertFn(value);
        current.value = "";
    };
    return (
        <>
            <h1>
                Awesome Todo List
            </h1>
            <div className="todo_input_box">
                <input 
                    className="todo_input"
                    type="text" 
                    placeholder="What do you need to do today?" 
                    ref={ inputRef }
                    onKeyDown={ (e: KeyboardEvent<HTMLInputElement> | undefined) => {
                        if (e?.code?.toLocaleLowerCase() === "enter") {
                            insert();
                        }
                    }}
                />
                <div
                    className="todo_add_btn"
                    onClick={ insert }
                >
                    Add
                </div>
            </div>
        </>
    );
}

const TodoCard: FC<{info: TodoType, checkFn:(id: string) => Promise<void>, removeFn: (id: string) => Promise<void> }> = ({ info, checkFn, removeFn }) => {
    return (
        <article className={`todo_card ${info.check? "check": ""}`}>
            <div
                className={`check_box`}
                onClick={ () => checkFn(info.id) }
            ></div>
            <div className="todo_content">
                {
                    info.content
                }
            </div>
            <div
                className="delete_btn"
                onClick={ ()=> removeFn(info.id) }
            >
                X
            </div>
        </article>
    )
}

export default TodoList;

겁나 길다 ㅎㅎ; writer 같은 경우는 window.LocalStroage를 이용해 caching 했고 B/E와 통신하기 위해 Server라는 Context를 만들어 봤는데 이점은 로깅??? 정도기 때문에 확실히 낭비 같았다. Server Context를 만들지 말고 Todo Context를 만드는 게 더 이득이었을 뜻 ㅋㅋㅋ

나중에 월루 할 기회가 생기면 리뷰한 내용에 소셜로그인을 붙여볼 생각이다. 그때까진 똥코드로 남겨둬야겠다. ㅎㅎ 부산에서 여행 중 심심하기도 하고 마무리를 안 해서 카페에 와 마무리 중인데 사진 몇 장 공유하는 것으로   골든래빗 《Tucker의 Go 언어 프로그래밍》 묘공단 스터디를 마무리하겠다.???

012345678


추후 우리 묘공단 스터디 오프라인 모임이 있으니 그것도 리뷰해야 하나?? ;; ㅋㅋ
코드 리뷰나 질문은 언제나 환영입니다.

 

'Golang > Tucker' 카테고리의 다른 글

[묘공단] 6주차 (2)  (0) 2023.11.12
[묘공단] 6주차 (1)  (0) 2023.11.05
[묘공단] 5주차 (3)  (0) 2023.10.29
[묘공단] 5주차 (2)  (0) 2023.10.29
[묘공단] 5주차 (1)  (1) 2023.10.29