컴포넌트에 mock 데이터를 삭제한 후 다음의 코드를 추가합니다.
import axios from 'axios';
...
const posts = ref()
onMounted(async () => {
posts.value = await getPosts();
});
...
const id = ref(1)
const getPosts = async () => {
try {
const response = await axios.get('http://192.168.15.246:8080/posts/' + id.value);
posts.value = response.data;
} catch (error) {
console.error(error);
}
}
...
const timeFormatChange = (time) => {
const date = new Date(time);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = ("0" + date.getHours()).slice(-2);
const minute = ("0" + date.getMinutes()).slice(-2);
return `${year}년 ${month}월 ${day}일 ${hour}시 ${minute}분`;
}
script setup
쪽에 다음을 추가해 http://192.168.15.246:8080/posts/1
경로로 부터 블로그 게시물 리스트를 받아오려고 해봅시다.
<template>
<div v-for="post in posts" :key="post.content" class="border-round-lg mx-5 my-5 p-4 shadow-2 flex flex-column gap-2"
:class="darkMode" style="height: 120px;">
<div class="font-bold text-3xl" style="height: 30%;">
{{ post.matter.Title }}
</div>
<div class="text-500 line-height-3 line w-full" style="height: 50%;">
<div v-if="post.matter.Content == ''">
{{ post.matter.Title }}
</div>
<div v-else>
{{ post.matter.Content }}
</div>
</div>
<div class=" text-500 text-xs flex align-items-center" style="height: 20%;">
{{ timeFormatChange(post.matter.Date) }}
</div>
</div>
</template>
또한 백엔드 서버에서 구현한 응답의 자료 구조에 따라 template
쪽도 수정합니다.
만약 vue.js와 Go를 저와 같이 서로 다른 서버에서 돌리고 있었다면, CORS 에러 혹은 ‘~ is not allowed by Access-Control-Allow-Origin’ 메세지를 마주하셨을 것 입니다. 이는 웹상에서 리소스를 주고 받을 때 보안적인 측면에서 강제되는 요소인데요. 본래 origin이 다른 출처(사이드)들 사이에서는 리소스를 공유할 수 없지만, CORS 정책을 지키고 있을 때는 허용이 됩니다.
console.log(location.origin);
을 실행해보면, 현재 사이트가 실행되고 있는 주소가 표시가 될 것인데요, 현재 제가 돌리고 있는 프론트엔드의 개발 ip 주소는 192.168.15.248:5174
로 백엔드 서버와 같은 서브넷 대역이긴 하지만 ip 가 다릅니다. 그리고 추후에는 GitHub.io를 이용해 블로그를 구동할 텐데, 이렇게 되면 아예 다른 지역의 서버를 이용하게 되겠습니다.
기본적으로 웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에
Origin
이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더의
Access-Control-Allow-Origin
이라는 값에 “이 리소스를 접근하는 것이 허용된 출처”를 내려주고, 이후 응답을 받은 브라우저는 자신이 보냈던 요청의Origin
과 서버가 보내준 응답의Access-Control-Allow-Origin
을 비교해본 후 이 응답이 유효한 응답인지 아닌지를 결정한다.⇒ CORS 정책 위반으로 인한 문제를 해결하는 가장 대표적인 방법은, 그냥 정석대로 서버에서
Access-Control-Allow-Origin
헤더에 알맞은 값을 세팅해주는 것이다. - 출처
그렇다면 저희는 만들어둔 Go 서버에 Header 을 조작할 수 있는 미들웨어를 추가 해 주는 것이 좋겠습니다. 미들웨어는 일종의 중간과정 이라고 생각하시면 되는데요. 꼭 Go 에서 뿐 만 아니라, 많은 다른 웹 서버 패키지에서도 즐겨 사용하는 기법입니다.
c.Header("Access-Control-Allow-Origin", *)
를 사용해 모든 사이트에 대해 허용하는 방법도 있겠지만, 위험한 방법일테니 저는 두 가지 특정 경로에 대해서만 허용 했습니다. 개발용 하나와 GitHub.io를 연결한 도메인 하나 입니다. (GitHub.io에 프로젝트를 연결하는 법은 나중에 확인해보겠습니다)
func main() {
...
r.Use(CORSMiddleware())
...
}
func CORSMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
allowUrlList := []string{"http://192.168.15.248:5174", "https://kubesy.com"}
var allowUrl string
for _, url := range allowUrlList {
if c.Request.Header.Get("Origin") == url {
allowUrl = url
break
}
}
// c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, Origin")
// c.Header("Access-Control-Allow-Credentials", "true")
c.Header("Access-Control-Allow-Origin", allowUrl)
c.Header("Access-Control-Allow-Methods", "GET, DELETE, POST")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}
제대로 posts 목록이 나타납니다! 이제 카드를 클릭하면 게시글을 읽을 수 있게 코드를 수정할 것 입니다. 카드 div 위에 @click="seePostDetail(post.path)”
를 추가 해주세요. 아, 그전에 go code에서 응답으로 주는 path 요소를 게시글 폴더 이름으로 받아오게끔 수정 했습니다.
카드를 클릭하면 router 기능에 의해 화면이 변환 됩니다.
const seePostDetail = (path) => {
router.push({ name: 'postDetail', params: { id: path } })
}
postDetailView.vue 에서는 다음 부분을 추가하고 수정 해줍시다.
첫번 째로 렌더러 코드 부분에서 버그 핸들링을 위해 다음과 같이 변경합니다.
const codeContent = language ? hljs.highlight(code, { language }).value : hljs.highlightAuto(code).value;
두 번째로 onMounted 안에서 코드를 다음과 같이 수정 해줍시다. ip가 코드 상에 하드코딩 되어 있는 것은 좋지는 않습니다. 추후에 실제로 서비스 될 때는 env 등을 이용해 숨겨 둘 필요가 있습니다.
const server = 'http://192.168.15.246:8080/markdown'
const path = "/" + route.params.id + "/" + route.params.id + ".md"
이 정도만 수정해도 백엔드와의 호출이 잘 이루어지고 있음을 확인할 수 있게 됩니다.
참고로, window.navigator.clipboard.writeText 기능은 HTTPS 을 사용하거나, localhost 일 때만 사용이 가능하다고 합니다. 깃허브 블로그에선 https를 사용하니 괜찮을 겁니다.
이제 이미지도 관리해봅시다. markdown 파일에는 이미지의 경로를 blog_data 하위만 적어주고, 이를 렌더링 할 때 경로를 바꿔줍시다. 저희의 백엔드 경로로 되어있습니다. 스타일도 걸어줍시다.
renderer.image = function (href, title, text) {
const path = 'http://192.168.15.246:8080/image/' + href
return `<div class="flex justify-content-center"><img src="${path}" alt="${text}" title="${title}" class="img border-1 border-round z-2 border-300" /></div>`; // for local references
};
이 정도면 어느 정도 게시글 관리 체계가 잡힌 것 같습니다.
저는 이미 Hugo로 만든 사이트를 제 계정의 GitHub page 로 연동 시켜두었습니다. 하지만, 이제 그걸 뒤집어 엎을 때가 온 것 같습니다. Tag, Archive, Search 기능을 구현하지 않았지만, 블로그의 본질인 게시글 보여주는 기능이 완성이 되었으니까요!
우선 GitHub page 연동에 이 설명을 많이 참고 했었습니다. 링크의 블로그를 필히 확인하시여, GitHub에 적절한 레포지토리를 생성해 주세요. Hugo의 가장 큰 단점은, markdown을 수정할 때 마다 git push를 새로 해줘야 한다는 점이었는데요. 이제 저희는 게시글을 따로 관리하기 때문에, 홈페이지의 기능 업데이트가 있지 않는 한 굳이 push를 해줄 필요는 없습니다.
자 이제, npm run build
로 만들어 놓은 Vue.js를 빌드해봅시다. 정상적으로 빌드가 되었다면, git에 추적되지 않는 dist 폴더가 생길 것 입니다. .gitignore 파일 안에서 dist를 무시하는 것을 지워야 서브모듈화가 가능합니다.
(아니면, 기존 레포지토리를 지우고 아예 새로 파는 것도 좋은 방법입니다)
그리고 dist 폴더 내용물들을 https://github.com/Larshavin/Larshavin.github.io 로 넣어줘야 합니다. Hugo 블로그 관리시에는 깃 서브 모듈을 사용하여 관리 했었습니다. 기존의 깃 서브 모듈을 지우고, 새로 연결해 봅시다. 폴더 구조는 다음과 같습니다. 에셋 폴더 안의 내용물은 표기 생략 했습니다.
.
├── assets
├── favicon.ico
└── index.html
원래 GitHub page 경로는 {username}.github.io
가 디폴트입니다. 만약 사놓은 DNS 가 있다면, 설정에 들어와 Pages의 Custom domain 영역을 바꿔줘야 합니다.
이제 화면이 잘 나오는 지 확인해봅시다. 저 같은 경우에는 잘 나오지 못했습니다. 공허함을 전해주는 에러 뿐이었죠.
⇒ Mixed Content: The page at 'https://kubesy.com/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'http://192.168.15.246:8080/posts/1'. This request has been blocked; the content must be served over HTTPS.
에러의 가능성 확실한 포인트는 에러 메세지 그대로 Mixed Content 이슈 입니다. HTTPS 연결로 로드된 HTML에서 다른 리소스를 HTTP 연결을 통해 로드할 때 발생한다고 합니다.
두 번째는 192.168.15.246은 private 서브넷 아이피 대역 이라는 점 입니다. 만약 위의 문제가 해결되었다고 해도, 192.168.15.246 경로가 저희 백엔드 서버를 가리킨다고 생각하지 못할 것 입니다. 즉 설정 부분 중 아래 체크를 풀어도, 당장 백엔드와 연결될 수 없다는 말 입니다.
꽤나 무력 해지지만 괜찮습니다. 다음 단계에서 HTTPS 지원과 함께 저희의 백엔드를 외부로 공개하는 과정을 연구해 봅시다.
HTTPS 통신을 하기 위해서는 SSL 인증서가 필요합니다. 과거에는 이걸 돈주고 많이 샀다고 하는데요 (저는 사본 적이 없긴 합니다). 요즘은 비영리 기관인 Let’s Encrypt를 많이 이용한다고 합니다. 왜냐하면 무료거든요.
Go에서 이 연결을 쉽게 해결해 줄 수 없을까요? 찾아보니 gin-gonic/autotls와 같은 패키지가 존재하는 걸 확인 할 수 있었습니다. 이걸 저희 쪽 백엔드에 한 번 적용 시켜볼까 합니다. 예제 코드는 다음과 같습니다.
package main
import (
"log"
"net/http"
"github.com/gin-gonic/autotls"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
// Ping handler
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})
log.Fatal(autotls.Run(r, "[domain]"))
}
[domain]
에 들어가는 부분은 제가 공인 아이피와 연결하도록 준비한 도메인 입니다. DNS가 없다면 https 관련 작업이 매우 힘들어집니다. (Let’s Encrypt에서는 불가능 한 건가 싶기도 합니다) 가비아 사이트를 들어가보면 1년에 2만원 정도 하는 도메인들이 많습니다.
위의 코드를 돌려보면, acme/autocert: missing certificate
에러들이 튀어 나옵니다. 제 도메인으로 인증받은 파일이 없기 때문이 아닌가 생각이 듭니다. ( 정말 그럴까요? - 코드 내부적으로 HTTP-01 챌린지를 사용하고 있는 것 아닐까요? 관련 내용이 아래에서도 나옵니다.)
라이브러리가 자동으로 인증해주나 싶었지만, 제 환경에서 불가능하네요. 그 원인은 문서 아래 설명되어 있긴 합니다. 왠지 다른 유명 웹서버의 대용으로 사용하기 의한 라이브러리 같습니다. 에잇, 이러면 기능적으로 굳이 Go 서버를 https 서버로 만들 필요가 없겠습니다.
아무튼, SSL 인증 받는 것이 우선 입니다. 직접 Certbot을 이용하면, Let’s Encrypt에서 사용할 수 있는 인증서를 발급 해준다고 합니다.
저는 Envoy proxy 라는 경량화 서버를 사용할 것 입니다. 그래서 Other 를 선택했습니다. 이 외에 nginx, apache, haproxy 등등의 선택지가 주어집니다. (사실 다른 웹서버를 구축 해놓으면 Github Page를 사용할 이유가 없 .. ㅎㅎㅎ .. hugo로 편리함을 누리시던 여러분은 지금 악의 구렁텅이로 흘러 들어가고 있습니다)
그림은 CertBot을 사용할 때 필요로 하는 요소들에 대해 설명하고 있습니다. 지금 마음에 걸리는 부분 중 하나는 제가 내부 서버를 공인아이피와 연결하기 위해 포트포워딩으로 80포트나 443포트를 이용하고 있다는 점이겠습니다. 왜냐면 지금 공인 아이피 밑에 사설 서브넷을 만들어 여러 사람과 공유하고 있기 때문 입니다. 저 혼자 공인 아이피의 80, 443 포트를 소유할 수 없습니다.
Certbot 설치에 있어서 snapd 사용을 권장하고 있습니다.
sudo dnf install epel-release
sudo dnf upgrade
sudo dnf install snapd
sudo systemctl enable --now snapd.socket
sudo ln -s /var/lib/snapd/snap /snap
그 다음은 snap 을 이용해 certbot을 설치합니다.
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
이제 인증을 받아야하는 순간이 왔습니다. 홈페이지에서는 다음으로 인증 받으라고 하지만, 공인 아이피에 포트 포워딩을 붙혀 사용하고 있는 저에게는 통하지 않는 인증 방식 입니다. 도메인에 해당하는 공인아이피의 80포트가 제 서버와 연결되어 있지 않기 때문입니다.
# 80 포트를 사용하고 있는 웹서버가 멈춰도 될 때
sudo certbot certonly --standalone
# 멈추면 안될 때
sudo certbot certonly --webroot
Let’s encrypt 에는 두 가지 챌린지가 존재 합니다. **HTTP-01** 와 DNS-01 **challenge** 입니다. 저는 포트포워딩을 사용하고 있기 때문에 후자를 사용해야 합니다. 가비아에서 구매한 DNS의 DNS 서버를 Cloudflare DNS 서버로 변경 (무료)의 과정을 수행해주셔야 합니다.
그리고 이곳에 나온 대로 DNS-01 챌린지에 대한 과정을 따라가 주셔야 합니다. 핵심은 도메인 설정에 TXT type
을 추가해야 하는데, host는 _acm3-challenge와 같고, sudo certbot certonly --manual --preferred-challenges dns -d "*.[domain]”
명령을 쳤을 때 암호가 나오는 영역을 내용으로 등록해줘야 한다는 점이겠습니다.
...
Please deploy a DNS TXT record under the name:
_acme-challenge.[domain].
with the following value:
#[암호가 나오는 영역]
Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.[domain].
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/[domain]/fullchain.pem
Key is saved at: /etc/letsencrypt/live/[domain]/privkey.pem
This certificate expires on 2023-10-24.
These files will be updated when the certificate renews.
NEXT STEPS:
- This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certificate, repeat this same certbot command before the certificate's expiry date.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
If you like Certbot, please consider supporting our work by:
* Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate
* Donating to EFF: https://eff.org/donate-le
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Certificate is saved at: /etc/letsencrypt/live/[domain]/fullchain.pem
Key is saved at: /etc/letsencrypt/live/[domain]/privkey.pem
두 개의 파일이 생성 되었습니다!
웹서버 envoy 를 다루기 전에, Golang gin으로 https 서버 만드는 다음 예제를 따라 해봅시다.
package main
import (
"github.com/gin-gonic/gin"
)
var (
SSLCRT string = "/etc/letsencrypt/live/[domain]/fullchain.pem"
SSLKEY string = "/etc/letsencrypt/live/[domain]/privkey.pem"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
})
r.RunTLS(":443", SSLCRT, SSLKEY)
}
# go run .
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTPS on :443
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
$ curl -k https://[domain]:[forwarding port]/ping
{"message":"pong"}
와! 작동합니다.
그런데 브라우저에서 보면 다음과 같은 화면이 뜨네요.
… 이 페이지 아래서 안전하지 않음으로 이동하면 본래대로 응답을 확인 할 수 있겠습니다. 허나 왜 안전하지 않을까요? HTTPS가 사용되지 않았다고 뜹니다. .[domain] 까지만을 ssl 인증서가 커버하는데, 경로에 에 해당하는 요소를 적어놓지 않았기 때문이네요. 귀찮더라도 www.[domain] 까지 쳐줘야 하나봅니다. 네, 잘 작동합니다.
그럼 이제 여기서 저희 지금의 Go 백엔드에 적용 테스트 까지는 해볼 법 하겠죠? kubesy.com 으로 접속해봅시다.
괜찮아 보이지만, 문제가 하나 있습니다. GitHub Page는 SPA 기능을 지원하지 않습니다. 만약 특정 경로의 화면에서 새로고침을 하게 되면, 다음과 같은 페이지가 뜨게 되는데요. 이를 해결하기 위해서는 vue router 에서 hash 히스토리 모드를 사용해야 한다고 합니다. 다만 hash 히스토리 모드는 SEO에 매우 좋지 않은 영향을 준다고 하죠.
더 쉽고 좋은 다른 방법은 Index.html을 404.html 로 복사하면 됩니다. 저는 쉘 스크립트로 Vue.js 빌드부터 git push 까지 과정을 다음과 같이 만들어 놓았습니다.
#!/bin/sh
echo -e "\033[0;32mBuild vue.js ...\033[0m"
# Build the project.
npm run build &&
# Go To dist folder
cd dist
# restore CNAME.
git restore CNAME
# Copy 404.html as same index.html
cp index.html 404.html
echo -e "\033[0;32mDeploying updates to GitHub...\033[0m"
# Add changes to git.
git add .
# Commit changes.
msg="rebuilding site `date`"
if [ $# -eq 1 ]
then msg="$1"
fi
git commit -m "$msg"
# Push source and build repos.
git push origin main
# Come Back up to the Project Root
cd ..
# blog 저장소 Commit & Push
git add .
msg="rebuilding site `date`"
if [ $# -eq 1 ]
then msg="$1"
fi
git commit -m "$msg"
git push origin main
와 정상적으로 잘 작동합니다. GitHub Page를 이용한 vue.js 코드에 백엔드 API를 연결했습니다! 여기서 이제 이 시리즈를 끝내볼까요??
… 잠깐 !!! 혹시 이걸 잊으신 건 아니겠죠? envoy가 저희를 기다리고 있었습니다 !
‘아니, 잘 작동하는 데 왜 굳이 더 과정을 만들어야 하는거야?’ 라고 생각하실 수 있으시겠지만, 저는 모든 백엔드 연결을 포트 포워딩으로 만들고 싶지 않습니다. 주소에 포트를 더럽게 쓰고싶지도 않고요.
만약 GitHub Page를 사용하지 말고 프론트엔드 까지 내가 관리하고 싶다면 다음 단계를 필요로 합니다. 이 내용에 대해선 ‘Proxy를 만들어보자’ 시리즈로 찾아 뵙겠습니다.
블로그 기능 구현에도 더 많은 과정이 남아 있습니다. 아직 저의 블로그는 Hugo를 사용할 때보다 기능적으로 더 부족한 상황입니다. 다음에는 어떤 기능을 만들어볼까요?