시나리오
1. Team Home이 운영하는 정적 파일을 제공하는 웹 서버 (localhost:3001)
- pages: /index.html
2. Team Jobs이 운영하는 정적 파일을 제공하는 웹 서버 (localhost:3002)
- fragments: /jobs/fragment/recommendation/index.html
3. Nginx Reverse Proxy Server (localhost:3000)
- Nginx를 Docker로 실행
루트 프로젝트 초기 세팅
다음 명령어를 순차적으로 실행해서 프로젝트를 구축한다.
pnpm init
corepack use pnpm@8.10.0
pnpm add turbo -D
`pnpm-workspace.yaml`을 통해 워크스페이스 설정을 추가한다.
packages:
- "teams/*"
- "apps/*"
그리고 모든 워크스페이스를 한번에 실행할 수 있도록 turbo.json을 추가해준다.
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"dev": {
"cache": false,
"persistent": true
}
}
}
nginx-ssi-app 프로젝트 세팅
루트 프로젝트에 apps/nginx-ssi-app 디렉토리를 만들고 프로젝트를 초기화한다.
pnpm init
proxy-server.conf 파일을 추가한다.
upstream team_home {
server host.docker.internal:3001;
}
upstream team_jobs {
server host.docker.internal:3002;
}
server {
listen 3000;
ssi on;
location /jobs/ {
proxy_pass http://team_jobs;
}
location / {
proxy_pass http://team_home;
}
}
Dockerfile를 만든다.
FROM nginx:1.25.3-alpine
COPY proxy-server.conf /etc/nginx/conf.d/proxy-server.conf
EXPOSE 3000
package.json에 build와 dev 명령어를 추가한다.
{
"name": "nginx-ssi-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "docker build -t nginx-ssi-app .",
"dev": "docker run -p 3000:3000 nginx-ssi-app",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
아래의 명령어를 통해 Dockerfile을 기반으로 이미지를 빌드하자
pnpm --filter nginx-ssi-app build
teams 워크스페이스 세팅
루트 디렉토리에 teams, teams/team-home teams/team-jobs 디렉토리를 각각 만들고 각각의 워크스페이스(team-home, team-jobs)를 `pnpm init`으로 초기화한다.
그리고 아래의 명령어를 통해 serve 패키지를 각 워크스페이스에 설치를 한다.
pnpm --filter team-home add serve
pnpm --filter team-jobs add serve
그리고 각 워크스페이스에 dev 명령어를 추가해준다.
# team/team-home
{
"name": "team-home",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "serve public -p 3001",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"serve": "^14.2.4"
}
}
# team/team-jobs
{
"name": "team-jobs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "serve public -p 3002",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"serve": "^14.2.4"
}
}
#include를 포함한 index.html 만들기
team-home의 public 디렉토리에 index.html과 index.css를 추가한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<link rel="stylesheet" href="/index.css" />
</head>
<body>
<div id="app">
<div id="main">
<h1>Home</h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/jobs/">Jobs</a></li>
</ul>
</nav>
</div>
<div id="team-jobs-recommendation">
<!-- #include virtual="/jobs/fragments/recommendation" -->
</div>
</div>
</body>
</html>
웹서버 (nginx)가 `<!--#include virtual="/jobs/fragments/recommendation" -->
` 부분을 보고 다른 경로의 HTML Fragment를 서버에서 불러와서 삽입해준다.
즉, 서버가 페이지를 클라이언트(브라우저)로 보내기 전에 조립해버린다. (즉, 서버단에서 Fragment를 붙이는 방식이다)
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
display: flex;
flex-direction: row;
justify-content: center;
gap: 20px;
}
#main {
width: 600px;
height: 600px;
background-color: white;
border-radius: 10px;
color: black;
text-align: center;
}
#team-jobs-recommendation {
width: 400px;
height: 220px;
background-color: white;
border-radius: 10px;
}
#team-jobs-recommendation .error {
color: black;
}
#include로 포함될 템플릿 만들기
team-jobs/public/jobs/fragments/recommendation 디렉토리에 index.html를 추가한다.
<div id="jobs-fragment-recommendation">
<h2>추천 채용공고</h2>
<div class="recommendations"></div>
</div>
이 상태로 실행을 하면 다음과 같이 Fragment가 `<!--#include >` 부분에 삽입 된다.
#include로 포함될 템플릿에 css와 js 추가하기
public/jobs/fragments/recommendation에 index.css를 추가하자.
#jobs-fragment-recommendation {
width: 400px;
height: 220px;
background-color: white;
border-radius: 10px;
color: black;
}
#jobs-fragment-recommendation h2 {
margin: 0;
padding: 20px;
}
#jobs-fragment-recommendation .recommendations {
display: flex;
flex-direction: column;
gap: 2px;
}
#jobs-fragment-recommendation .recommendations div {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
background-color: black;
color: white;
}
그리고 index.css를 html에 연동해주도록 하자. 이로써 스타일을 추가할 수 있게 되었다.
<link rel="stylesheet" href="/jobs/fragments/recommendation/index.css" />
<div id="jobs-fragment-recommendation">
<h2>추천 채용공고</h2>
<div class="recommendations"></div>
</div>
그리고 public/jobs/fragments/recommendation에 index.js를 추가해주고 html에 연동해주도록 하자.
fetch("/jobs/api/recommendations.json")
.then((res) => res.json())
.then((recommendations) => {
document.querySelector(
"#jobs-fragment-recommendation .recommendations"
).innerHTML = recommendations
.map(
({ name, url }) =>
`<div><a href="${url}" target="_blank">${name}</a></div>`
)
.join("");
});
<link rel="stylesheet" href="/jobs/fragments/recommendation/index.css" />
<div id="jobs-fragment-recommendation">
<h2>추천 채용공고</h2>
<div class="recommendations"></div>
</div>
<script src="/jobs/fragments/recommendation/index.js"></script>
마지막으로 public/jobs/api에 recommendations.json을 추가해주도록 하자.
[
{ "name": "패스트캠퍼스", "url": "https://fastcampus.co.kr/" },
{
"name": "네이버",
"url": "https://recruit.navercorp.com/"
},
{ "name": "카카오", "url": "https://careers.kakao.com/" }
]
그 결과 다음과 같은 화면을 볼 수 있게 되었다.
Ajax와의 차이점
클라이언트로 응답을 보낸 후에 Fragment를 가져오는 Ajax와 달리 서버에서 모든 처리를 한 후에 응답을 내려준다.