๊ฐ์ธ์ ์ธ ๊ณ ๋ฏผ๊ณผ ๊ฐ๋ฐ์ ์ฃผ์ ๋ก ์๋ก ์ํตํ๋ ์ปค๋ฎค๋ํฐ ํ๋ก์ ํธ์ ๋๋ค. Vanilla JavaScript๋ฅผ ์ฌ์ฉํ์ฌ ๊ตฌํํ์ผ๋ฉฐ, ํ๋ก ํธ์๋์ ๋ฐฑ์๋๋ฅผ ์ง์ ๊ฐ๋ฐํ์ต๋๋ค.
- ๊ฐ๋ฐ๊ธฐ๊ฐ: 2025-10-01 ~ 2025-12-08
- ๊ฐ๋ฐ ์ธ์: ํ๋ก ํธ์๋/๋ฐฑ์๋ 1๋ช (๋ณธ์ธ)
- ์ธ์ด: Vanilla JavaScript (ES6+)
- ์คํ์ผ๋ง: CSS3, Bootstrap 5.3.8
- ์๋ฒ: Express.js 5.1.0 (์ ์ ํ์ผ ์๋น)
- ํ ์คํธ: Jest 30.2.0, Supertest 7.1.4
- Spring Boot, AWS (S3, CloudFront)
- ๋ฐฑ์๋ Github ์ ์ฅ์
- (์ฒจ๋ถ ์์ )
- JWT ๊ธฐ๋ฐ ์ธ์ฆ (Access Token + HttpOnly Refresh Token)
- ์๋ ํ ํฐ ๊ฐฑ์ (Refresh Token ํ์ฉ)
- ์ด๋ฉ์ผ ์ธ์ฆ ์ฝ๋ ๋ฐ์ก ๋ฐ ๊ฒ์ฆ
- ํ์ ํํด ๋ฐ ๋ณต๊ตฌ ๊ธฐ๋ฅ
- ๊ฒ์๊ธ ์์ฑ, ์กฐํ, ์์ , ์ญ์ (CRUD)
- ์ปค์ ๊ธฐ๋ฐ ๋ฌดํ ์คํฌ๋กค ํ์ด์ง๋ค์ด์
- ์ต์ ์/์ธ๊ธฐ์ ์ ๋ ฌ
- ๊ฒ์๊ธ ์ข์์ ๊ธฐ๋ฅ
- ์กฐํ์ ์นด์ดํ
- ๋๊ธ ์์ฑ, ์์ , ์ญ์
- ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
- ์์ฑ์ ํ์ ๋ฐ ๋ณธ์ธ ๋๊ธ ๊ตฌ๋ถ
- ์ ๋ก๋: AWS S3 Presigned URL ํ์ฉ
- ์กฐํ: CloudFront CDN + Signed Cookie ์ธ์ฆ
- ํ๋กํ ์ด๋ฏธ์ง ๋ฐ ๊ฒ์๊ธ ์ด๋ฏธ์ง ์ง์
- ํ๋กํ ์ด๋ฏธ์ง ๋ณ๊ฒฝ
- ๋๋ค์ ๋ณ๊ฒฝ
- ๋น๋ฐ๋ฒํธ ๋ณ๊ฒฝ
- ํ์ ํํด
ํด๋ ๊ตฌ์กฐ
โโโ .dockerignore
โโโ .github/
โโโ .gitignore
โโโ Dockerfile
โโโ jest.config.js
โโโ jsconfig.json
โโโ package.json
โโโ server.js # Express ์ ์ ํ์ผ ์๋ฒ
โโโ tests/
โ โโโ server.test.js # ์๋ฒ ํ
์คํธ
โโโ public/
โโโ index.html # ๋ฉ์ธ ํ์ด์ง
โโโ css/
โ โโโ reset.css # CSS ๋ฆฌ์
โ โโโ theme.css # ํ
๋ง ๋ณ์
โ โโโ layout.css # ๋ ์ด์์ ์คํ์ผ
โโโ js/
โ โโโ config.js # API ํ๊ฒฝ ์ค์
โ โโโ api.js # API ์์ฒญ ํธ๋ค๋ฌ
โ โโโ cdn.js # CDN ์ด๋ฏธ์ง ๋ก๋ (Signed Cookie)
โ โโโ dom.js # DOM ์ ํธ๋ฆฌํฐ
โ โโโ common.js # ๊ณตํต ์ ํธ๋ฆฌํฐ
โ โโโ storage.js # ๋ก์ปฌ ์คํ ๋ฆฌ์ง ๊ด๋ฆฌ
โโโ components/
โ โโโ navbar.html # ๋ค๋น๊ฒ์ด์
๋ฐ
โ โโโ navbar.css
โ โโโ post-card.css # ๊ฒ์๊ธ ์นด๋ ์คํ์ผ
โ โโโ mypage-dropdown.css
โโโ pages/
โโโ home/ # ํ ํ์ด์ง
โ โโโ index.html
โ โโโ home.html
โ โโโ home.js
โ โโโ home.css
โโโ auth/ # ์ธ์ฆ ๊ด๋ จ ํ์ด์ง
โ โโโ login/
โ โ โโโ login.html
โ โ โโโ login.js
โ โ โโโ login.css
โ โโโ signup/
โ โ โโโ signup.html
โ โ โโโ signup.js
โ โ โโโ signup.css
โ โโโ recover/
โ โโโ recover.html
โ โโโ recover.js
โโโ board/ # ๊ฒ์ํ ๊ด๋ จ ํ์ด์ง
โ โโโ board.html # ๊ฒ์๊ธ ๋ชฉ๋ก
โ โโโ board.js
โ โโโ board.css
โ โโโ postDetail.html # ๊ฒ์๊ธ ์์ธ
โ โโโ postDetail.js
โ โโโ postDetail.css
โ โโโ comment.js # ๋๊ธ ๊ด๋ฆฌ
โโโ mypage/ # ๋ง์ดํ์ด์ง
โโโ mypage.html
โโโ mypage.js
โโโ mypage.css
๊ตฌ์กฐ:
api.js: ๋ชจ๋ API ์์ฒญ ์ค์ ๊ด๋ฆฌcdn.js: CloudFront ์ด๋ฏธ์ง ๋ก๋ฉdom.js: DOM ์กฐ์ ์ ํธ๋ฆฌํฐstorage.js: ๋ก์ปฌ ์คํ ๋ฆฌ์ง ๊ด๋ฆฌcommon.js: ๊ณตํต ํจ์ (๋ ์ง ํฌ๋งทํ ๋ฑ)
| ๋ก๊ทธ์ธ | ํ์๊ฐ์ | ์ด๋ฉ์ผ |
|---|---|---|
![]() |
![]() |
| ๊ฒ์๊ธ ๋ชฉ๋ก | ๊ฒ์๊ธ ์์ธ |
|---|---|
![]() |
![]() |
| ๊ฒ์๊ธ ๋ฑ๋ก | ๊ฒ์๊ธ ์์ |
|---|---|
![]() |
![]() |
| ๋ณต๊ตฌ ํ์ | ํ์ ๋ณต๊ตฌ |
|---|---|
![]() |
![]() |
๋ฌธ์ : ๋ก์ปฌ ๊ฐ๋ฐ ํ๊ฒฝ์์ ๋ฐฑ์๋ API ํธ์ถ ์ CORS ์๋ฌ ๋ฐ์
ํด๊ฒฐ:
- ๋ฐฑ์๋์์ CORS ์ค์ ์ถ๊ฐ
credentials: "include"์ต์ ์ผ๋ก ์ฟ ํค ์ ์ก ํ์ฉ- CloudFront์์๋ CORS ํค๋ ์ค์
๋ฌธ์ : CloudFront์์ ์ด๋ฏธ์ง ๋ก๋ ์ 403 ์๋ฌ ๋ฐ๋ณต ๋ฐ์
ํด๊ฒฐ:
fetchAPI๋ฅผ ์ฌ์ฉํ์ฌcredentials: "include"์ค์ <img>ํ๊ทธ์src๋ฅผ ์ง์ ์ค์ ํ๋ ๋์ Blob URL ์ฌ์ฉ- ์ต์ด ์์ฒญ ์คํจ ์ ์๋์ผ๋ก Signed Cookie ๋ฐ๊ธ ๋ฐ ์ฌ์๋ ๋ก์ง ๊ตฌํ
// ์ด๋ฏธ์ง ์กฐํ ์ Signed Cookie ์๋ ์ฒ๋ฆฌ
async fetchImage(cdnBaseUrl, objectKey) {
let res = await fetch(url, { credentials: "include" });
// 401/403 ์ ์ฟ ํค ๋ฐ๊ธ ํ ์ฌ์๋
if ((res.status === 401 || res.status === 403) && !this._cookieReady) {
await this._ensureCookieOnce();
res = await fetch(url, { credentials: "include" });
}
return await res.blob();
}๋ฌธ์ : ์คํฌ๋กค ์ด๋ฒคํธ ๋ฐ์ ์ ์ค๋ณต API ํธ์ถ
ํด๊ฒฐ:
isLoadingํ๋๊ทธ๋ก ์ค๋ณต ์์ฒญ ๋ฐฉ์ง- Intersection Observer API ํ์ฉ ๊ณ ๋ ค
let isLoading = false;
async function loadMorePosts() {
if (isLoading || !hasNext) return;
isLoading = true;
try {
const data = await api.getPosts({ cursor: nextCursor });
// ... ๋ฐ์ดํฐ ์ฒ๋ฆฌ
} finally {
isLoading = false;
}
}์ด๋ฒ ํ๋ก์ ํธ๋ก ํ๋ก ํธ์๋๋ฅผ ์ฒ์ ๊ตฌํํ๋ฉด์ ๋ธ๋๋ฐ์ค ๊ฐ์๋ ํ๋ก ํธ์ํธ ์ธก์ ์์ ๊ณผ์ ์ ์ดํดํ๋ฉด์, ๋ฐฑ์๋ ๊ฐ๋ฐ์๋ก์ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์ ํ์ ์ ํ ๋, ์ด๋ป๊ฒ ํด์ฃผ๋ ๊ฒ์ด ์ข์์ง ๋ชธ์ ๋๊ปด๋ณผ ์ ์๋ ์๊ฐ์ด์๋ ๊ฒ ๊ฐ๋ค. ๋ํ ๋ณด์์ ์ธ ์ธก๋ฉด์์๋ ํ๋ก ํธ์๋ ๊ฐ๋ฐ์์์ ์ํต์ ํตํด์ ํ ์ชฝ์์ ๊ตฌ๋ฉ์ด ๋์ง ์๋๋ก ๋์ฑ ๋ง์ ์ํต์ด ํ์ํ๋ค๋ ๊ฒ์ ๋ค์ํ๋ฒ ๋๊ผ๋ค.
- ๊ฒ์๋ฌผ ๋ฆฌ์คํธ ํ์ด์ง์์ ๊ฒ์๋ฌผ ์์ฝ์ ๋ณด์ฌ์ฃผ๋๋ก ์์








