-
Notifications
You must be signed in to change notification settings - Fork 7
๐ชต 3. ์ธํ๋ผ ์ค์ต(5) : ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํด๋ณด๊ธฐ
-
VPC (10.0.0.0/16)
Public Subnet์ ์ธ๋ถ์ ์ฐ๊ฒฐ๋๋ ์๋น์ค, Private Subnet์ ๋ด๋ถ ๋ฆฌ์์ค๋ฅผ ์ํ ํ๊ฒฝ์ผ๋ก ๊ตฌ์ฑ
-
Public Subnet (10.0.1.0/24)
์ฌ์ฉ์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ ์น ์๋ฒ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์๋ฒ๊ฐ ์์น
-
Nginx
์ ์ ํ์ผ ์ ๊ณต ๋ฐ ๋ฆฌ๋ฒ์ค ํ๋ก์๋ก ๋์ํ์ฌ ์ฌ์ฉ์ ์์ฒญ์ ์ ์ ํ ์๋น์ค๋ก ๋ผ์ฐํ
-
NestJS
/socket.io
๋ฅผ ํตํด WebSocket ์์ฒญ์ ์ฒ๋ฆฌํ๋ฉฐ, API ์๋ฒ ์ญํ ์ํ. -
Redis (Private Subnet, 10.0.2.0/24)
ํด๋ผ์ฐ๋ ํ๊ฒฝ์ Redis๋ฅผ ํ์ฉํ์ฌ ์ธ์ ๊ด๋ฆฌ ๋ฐ ๋ฐ์ดํฐ ์บ์ฑ ์ฒ๋ฆฌ
-
Docker
Nginx์ NestJS ์ ํ๋ฆฌ์ผ์ด์ ๋ชจ๋ Docker ์ปจํ ์ด๋๋ก ์คํ๋๋ฉฐ, ์ด๋ฏธ์ง๋ Github Actions๋ฅผ ํตํด ๋น๋ ๋ฐ Docker Hub์์ ๊ด๋ฆฌ
-
๋ฐฐํฌ ์๋ํ
CI/CD ํ์ดํ๋ผ์ธ(Github Actions)์ ํตํด ์ ํ๋ฆฌ์ผ์ด์ ๋น๋ ๋ฐ ๋ฐฐํฌ ์๋ํ
FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.12.3 --activate
WORKDIR /app
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY core/package.json ./core/
COPY server/package.json ./server/
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm --filter @troublepainter/core build
RUN pnpm --filter server build
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/pnpm-workspace.yaml ./
COPY --from=builder /app/server/package.json ./server/package.json
COPY --from=builder /app/server/dist ./server/dist
COPY --from=builder /app/core/package.json ./core/package.json
COPY --from=builder /app/core/dist ./core/dist
RUN corepack enable && corepack prepare pnpm@9.12.3 --activate && cd server && pnpm install --prod
WORKDIR /app/server
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
CMD ["node", "dist/main.js"]
- ํ๋ก์ ํธ ๊ตฌ์กฐ: core์ server๊ฐ ํฌํจ๋ ๋ชจ๋ ธ๋ ํฌ ๊ตฌ์กฐ
- ๋น๋ ํ๋ก์ธ์ค: core ํ๋ก์ ํธ ๋น๋ ํ server ํ๋ก์ ํธ ๋น๋
-
ํ๋ก๋์
์ค์
- ๋น๋๋ core/server dist ํ์ผ๋ง ๋ณต์ฌ
- ํ๋ก๋์ ์์กด์ฑ๋ง ์ค์น (--prod ํ๋๊ทธ)
- server ๋๋ ํ ๋ฆฌ๋ฅผ ๋ฉ์ธ ์์ ๊ณต๊ฐ์ผ๋ก ์ค์
- ํ๊ฒฝ ๋ณ์: NODE_ENV=production, PORT=3000 ์ค์
- ํฌํธ: 3000๋ฒ ํฌํธ ๋ ธ์ถ
- ์คํ ๋ฐฉ์: Node.js๋ก server/dist/main.js ์คํ
- ์ต์ ํ: ๋ฉํฐ ์คํ ์ด์ง ๋น๋๋ก ๊ฐ๋ฐ ์์กด์ฑ ์ ์ธ
FROM node:20-alpine AS builder
RUN corepack enable && corepack prepare pnpm@9.12.3 --activate
WORKDIR /app
COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
COPY core/package.json ./core/
COPY client/package.json ./client/
RUN pnpm install --frozen-lockfile
COPY . .
ARG VITE_API_URL
ARG VITE_SOCKET_URL
RUN echo "VITE_API_URL=$VITE_API_URL" > client/.env && echo "VITE_SOCKET_URL=$VITE_SOCKET_URL" >> client/.env && pnpm --filter @troublepainter/core build && pnpm --filter client build
FROM nginx:alpine
COPY nginx.conf /etc/nginx/templates/default.conf.template
COPY --from=builder /app/client/dist /usr/share/nginx/html
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"]
- ํ๋ก์ ํธ ๊ตฌ์กฐ: core์ client๊ฐ ํฌํจ๋ ๋ชจ๋ ธ๋ ํฌ ๊ตฌ์กฐ
- ํ๊ฒฝ ๋ณ์: VITE_API_URL, VITE_SOCKET_URL์ ๋น๋ ์์ ์ ์ฃผ์
- ๋น๋ ํ๋ก์ธ์ค: core ํ๋ก์ ํธ ๋น๋ ํ client ํ๋ก์ ํธ ๋น๋
- ๋ฐฐํฌ ์ค์ : nginx ์น์๋ฒ๋ก ๋น๋๋ ํด๋ผ์ด์ธํธ ์ ํ๋ฆฌ์ผ์ด์ ์๋น
- ํฌํธ: HTTP(80), HTTPS(443) ํฌํธ ๋ ธ์ถ
- ์ต์ ํ: ๋ฉํฐ ์คํ ์ด์ง ๋น๋๋ก ์ต์ข ์ด๋ฏธ์ง ํฌ๊ธฐ ์ต์ํ
- ์คํ ๋ฐฉ์: nginx๋ฅผ ํฌ๊ทธ๋ผ์ด๋์์ ๊ตฌ๋
server {
listen 80;
server_name www.troublepainter.site;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name www.troublepainter.site;
ssl_certificate /etc/letsencrypt/live/www.troublepainter.site/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.troublepainter.site/privkey.pem;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://server:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
location /socket.io {
proxy_pass http://server:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
-
HTTP(80) ์ค์
- ๋ชจ๋ HTTP ์์ฒญ์ HTTPS๋ก ๋ฆฌ๋ค์ด๋ ํธ
- ๋๋ฉ์ธ: [www.troublepainter.site](http://www.troublepainter.site/)
-
HTTPS(443) ์ค์
- SSL ์ธ์ฆ์ ์ค์ (Let's Encrypt)
- gzip ์์ถ ํ์ฑํ (ํ ์คํธ, CSS, JS ๋ฑ)
- ๋๋ฉ์ธ: [www.troublepainter.site](http://www.troublepainter.site/)
-
๋ผ์ฐํ
๊ท์น
-
/
(ํ๋ก ํธ์๋)- ์ ์ ํ์ผ ์ ๊ณต
- SPA ๋ผ์ฐํ ์ง์ (/index.html๋ก ํด๋ฐฑ)
-
/api
(HTTP API)- server:3000์ผ๋ก ํ๋ก์
- HTTP ํต์ ์ ํ์ํ ๊ธฐ๋ณธ ํค๋๋ง ์ค์
-
/socket.io
(WebSocket)- server:3000์ผ๋ก ํ๋ก์
- WebSocket ์ฐ๊ฒฐ์ ์ํ upgrade ํค๋ ์ค์
-
- ๋ชจ๋ ํต์ ์ ํ๋์ Node.js ์๋ฒ(server:3000)์์ ์ฒ๋ฆฌํ๋ฉฐ, ์ฉ๋์ ๋ฐ๋ผ ๊ฒฝ๋ก๋ง ๊ตฌ๋ถ๋์ด ์์.
name: Client CI/CD
on:
push:
branches: [develop]
paths:
- 'client/**'
- 'core/**'
- 'nginx.conf'
- '.github/workflows/client-ci-cd.yml'
- 'Dockerfile.nginx'
jobs:
ci-cd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: '9'
- name: Install Dependencies
run: pnpm install --frozen-lockfile
- name: Lint Client
working-directory: ./client
run: pnpm lint | true
- name: Test Client
working-directory: ./client
run: pnpm test | true
- name: Docker Setup
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.nginx
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-nginx:latest
build-args: |
VITE_API_URL=${{secrets.VITE_API_URL}}
VITE_SOCKET_URL=${{secrets.VITE_SOCKET_URL}}
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: mira
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/mira/web30-stop-troublepainter
export DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-nginx:latest
docker compose up -d nginx
-
Workflow ํธ๋ฆฌ๊ฑฐ
- develop ๋ธ๋์น push ์ด๋ฒคํธ
- ํด๋ผ์ด์ธํธ, ์ฝ์ด, Nginx ๊ด๋ จ ํ์ผ ๋ณ๊ฒฝ ์์๋ง ์คํ
-
ci-cd
-
์ด๊ธฐ ์ค์
- Ubuntu ํ๊ฒฝ ์ค์
- Node.js 20 ์ค์น
- pnpm 9 ์ค์น
- ์์กด์ฑ ์ค์น
- core ํจํค์ง ๋น๋
-
์ฝ๋ ํ์ง ๊ฒ์ฆ
- ํด๋ผ์ด์ธํธ ๋ฆฐํธ ๊ฒ์ฌ
- ํด๋ผ์ด์ธํธ ํ ์คํธ ์คํ
- (์คํจํด๋ ํ์ดํ๋ผ์ธ ๊ณ์ ์งํ)
-
๋์ปค ์ด๋ฏธ์ง ๋น๋/๋ฐฐํฌ
- Docker Buildx ์ค์
- Docker Hub ๋ก๊ทธ์ธ
- Nginx ์ด๋ฏธ์ง ๋น๋ ๋ฐ ํธ์
- ํ๊ฒฝ๋ณ์ ์ฃผ์
-
์๋ฒ ๋ฐฐํฌ
- SSH๋ก ์๋ฒ ์ ์
- ์ต์ ์ด๋ฏธ์ง pull
- nginx ์ปจํ ์ด๋ ์ฌ์์
-
์ด๊ธฐ ์ค์
name: Server CI/CD
on:
push:
branches: [develop]
paths:
- 'server/**'
- 'core/**'
- '.github/workflows/server-ci-cd.yml'
- 'Dockerfile.server'
jobs:
ci-cd:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Setup pnpm
uses: pnpm/action-setup@v3
with:
version: '9'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Core Package
working-directory: ./core
run: pnpm build
- name: Create .env file
working-directory: ./server
run: |
echo "REDIS_HOST=${{ secrets.REDIS_HOST }}" >> .env
echo "REDIS_PORT=${{ secrets.REDIS_PORT }}" >> .env
echo "CLOVA_API_KEY=${{ secrets.CLOVA_API_KEY }}" >> .env
echo "CLOVA_GATEWAY_KEY=${{ secrets.CLOVA_GATEWAY_KEY }}" >> .env
- name: Run tests
run: pnpm --filter server test | true
- name: Docker Setup
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.server
push: true
tags: ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-server:latest
build-args: |
REDIS_HOST=${{ secrets.REDIS_HOST }}
REDIS_PORT=${{ secrets.REDIS_PORT }}
CLOVA_API_KEY=${{ secrets.CLOVA_API_KEY }}
CLOVA_GATEWAY_KEY=${{ secrets.CLOVA_GATEWAY_KEY }}
- name: Deploy to Server
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: mira
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/mira/web30-stop-troublepainter
export DOCKERHUB_USERNAME=${{ secrets.DOCKERHUB_USERNAME }}
docker pull ${{ secrets.DOCKERHUB_USERNAME }}/troublepainter-server:latest
docker compose up -d server
-
Workflow ํธ๋ฆฌ๊ฑฐ
- develop ๋ธ๋์น push ์ด๋ฒคํธ
- ์๋ฒ, ์ฝ์ด, Dockerfile ๊ด๋ จ ํ์ผ ๋ณ๊ฒฝ ์์๋ง ์คํ
-
ci-cd
-
์ด๊ธฐ ์ค์
- Ubuntu ํ๊ฒฝ ์ค์
- Node.js 20 ์ค์น
- pnpm 9 ์ค์น
- ์์กด์ฑ ์ค์น
- core ํจํค์ง ๋น๋
-
์ฝ๋ ํ์ง ๊ฒ์ฆ
- ์๋ฒ ํ๊ฒฝ๋ณ์(.env) ์ค์
- ์๋ฒ ํ ์คํธ ์คํ
- (์คํจํด๋ ํ์ดํ๋ผ์ธ ๊ณ์ ์งํ)
-
๋์ปค ์ด๋ฏธ์ง ๋น๋/๋ฐฐํฌ
- Docker Buildx ์ค์
- Docker Hub ๋ก๊ทธ์ธ
- Server ์ด๋ฏธ์ง ๋น๋ ๋ฐ ํธ์
- ํ๊ฒฝ๋ณ์ ์ฃผ์ (Redis, Clova API)
-
์๋ฒ ๋ฐฐํฌ
- SSH๋ก ์๋ฒ ์ ์
- ์ต์ ์ด๋ฏธ์ง pull
- server ์ปจํ ์ด๋ ์ฌ์์
-
์ด๊ธฐ ์ค์
services:
server:
image: ${DOCKERHUB_USERNAME}/troublepainter-server:latest
container_name: troublepainter_server
environment:
- NODE_ENV=production
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- CLOVA_API_KEY=${CLOVA_API_KEY}
- CLOVA_GATEWAY_KEY=${CLOVA_GATEWAY_KEY}
networks:
- app_network
restart: unless-stopped
nginx:
image: ${DOCKERHUB_USERNAME}/troublepainter-nginx:latest
container_name: troublepainter_nginx
volumes:
- /etc/letsencrypt:/etc/letsencrypt
ports:
- "80:80"
- "443:443"
depends_on:
- server
networks:
- app_network
restart: unless-stopped
networks:
app_network:
name: app_network
driver: bridge
- ์ฌ๋ฌ Docker ์ปจํ ์ด๋๋ฅผ ์ ์ํ๊ณ ๊ด๋ฆฌํ๊ธฐ ์ํ ์ค์ ํ์ผ
-
์๋น์ค ๊ตฌ์ฑ
- Server ์๋น์ค (Node.js ์ ํ๋ฆฌ์ผ์ด์ )
- Nginx ์๋น์ค (์น์๋ฒ/ํ๋ก์)
-
API ์๋น์ค ์ค์
- ์ปจํ ์ด๋๋ช : troublepainter_server
- ํ๊ฒฝ๋ณ์: NODE_ENV, Redis ์ฐ๊ฒฐ ์ ๋ณด, Clova API ์ธ์ฆํค
- ์๋ ์ฌ์์ ์ ์ฑ ์ ์ฉ
- app_network์ ์ฐ๊ฒฐ
-
Nginx ์๋น์ค ์ค์
- ์ปจํ ์ด๋๋ช : troublepainter_nginx
- SSL ์ธ์ฆ์ ๋ณผ๋ฅจ ๋ง์ดํธ (/etc/letsencrypt)
- 80/443 ํฌํธ ๋ ธ์ถ
- API ์๋น์ค์ ์์กด์ฑ ์ค์
- app_network์ ์ฐ๊ฒฐ
-
๋คํธ์ํฌ ์ค์
- ๋คํธ์ํฌ๋ช : app_network
- ํ์
: bridge ๋คํธ์ํฌ
- ๊ฐ์ Docker ํธ์คํธ ๋ด์์ ์คํ๋๋ ์ปจํ ์ด๋๋ค์ด ์๋ก ํต์ ํ ์ ์๊ฒ ํด์ฃผ๋ ๊ฐ์์ ๋คํธ์ํฌ
- Nginx๊ฐ ์ ์ ํ์ผ ํธ์คํ ๊ณผ ๋ฆฌ๋ฒ์ค ํ๋ก์ ๋ ๊ฐ์ง ์ญํ ์ํ
- ์๋ฒ ๋ฆฌ์์ค ์ฌ์ฉ ์ฆ๊ฐ
- ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ณ๊ฒฝ์๋ง๋ค Nginx ์ด๋ฏธ์ง ์ฌ๋น๋ ํ์
- ๋ฐฐํฌ ์๊ฐ ์ฆ๊ฐ, ๋ฆฌ์์ค ๋ญ๋น
- Nginx ์ค์ ๋ณ๊ฒฝ๋ง ํ์ํ ๋๋ ์ ์ฒด ๋น๋ ํ์
- HTTP API์ Socket.IO๊ฐ ํ๋์ ํ๋ก์ธ์ค์์ ๋์
- ํธ๋ํฝ ์ฆ๊ฐ ์ ๊ฐ๋ณ ์ค์ผ์ผ๋ง ๋ถ๊ฐ
- ํ ๊ธฐ๋ฅ์ ์ฅ์ ๊ฐ ์ ์ฒด ์๋น์ค์ ์ํฅ
-
์ ์ ํ์ผ ๋ถ๋ฆฌ
- ์ ์ ํ์ผ์ Nginx๊ฐ ์๋ AWS S3์ ๊ฐ์ ์ค๋ธ์ ํธ ์คํ ๋ฆฌ์ง์ ์ ๋ก๋ํ๊ณ , CDN(Content Delivery Network)์ ํ์ฉํด ์ ์ธ๊ณ์ ์ผ๋ก ๋น ๋ฅด๊ฒ ๋ฐฐํฌ
- ์ด๋ฅผ ํตํด Nginx๊ฐ ๋ฆฌ๋ฒ์ค ํ๋ก์ ์ญํ ์๋ง ์ง์คํ ์ ์๋๋ก ๊ฐ์
-
HTTPS ์ง์ ๊ฐํ
- Cloudflare ๊ฐ์ CDN ์๋น์ค์์ HTTPS๋ฅผ ์ ๊ณตํ๋๋ก ์ค์ ํด ๋ณด์ ๊ฐํ
- ๋ด๋ถ ๋คํธ์ํฌ์์๋ HTTP ํต์ ์ผ๋ก ์ ํํ์ฌ ๋ฆฌ์์ค ์ต์ ํ
-
Nginx ์ด๋ฏธ์ง์ ์ ์ ํ์ผ์ ๋ถ๋ฆฌ
- Nginx ์ด๋ฏธ์ง๋ ์ ์ ํ์ผ ์์ด ์ค์ ํ์ผ๋ง ํฌํจ
- ํด๋ผ์ด์ธํธ ๋น๋ ๊ฒฐ๊ณผ๋ฌผ์ Nginx ์ปจํ ์ด๋์ ์ธ๋ถ ๋ณผ๋ฅจ์ผ๋ก ์ฐ๊ฒฐํ์ฌ ์ด๋ฏธ์ง ์ฌ๋น๋ ์์ด ๋์ ์ผ๋ก ์ ๊ณต
-
๋ฐฐํฌ ์๋ํ
- CI/CD ํ์ดํ๋ผ์ธ์์ ์ ์ ํ์ผ ๋น๋ ํ ์คํ ๋ฆฌ์ง๋ก ์ ๋ก๋ํ๊ฑฐ๋ Nginx ์ปจํ ์ด๋์ ๋ณผ๋ฅจ์ ๋ฐ๋ก ๋ณต์ฌ
-
์๋น์ค ๋ถ๋ฆฌ
- HTTP API ์๋ฒ์ WebSocket ์๋ฒ(Socket.IO)๋ฅผ ๋ณ๋์ ํ๋ก์ธ์ค ๋๋ ์ปจํ ์ด๋๋ก ๋ถ๋ฆฌ
- ๊ฐ ํ๋ก์ธ์ค๋ ๋ ๋ฆฝ์ ์ผ๋ก ๋ฐฐํฌ ๋ฐ ์ค์ผ์ผ๋ง ๊ฐ๋ฅ
-
๋ก๋ ๋ฐธ๋ฐ์ฑ
- API ์๋ฒ์ WebSocket ์๋ฒ ์๋จ์ ๋ก๋ ๋ฐธ๋ฐ์๋ฅผ ๋์ด ํธ๋ํฝ์ ๊ท ๋ฑ ๋ถ๋ฐฐ
- Nginx๋ HAProxy, AWS ELB์ ๊ฐ์ ๋ก๋ ๋ฐธ๋ฐ์ ์ฌ์ฉ
- ์คํ ์ค์ผ์ผ๋ง
- 1. ๊ฐ๋ฐ ํ๊ฒฝ ์ธํ ๋ฐ ํ๋ก์ ํธ ๋ฌธ์ํ
- 2. ์ค์๊ฐ ํต์
- 3. ์ธํ๋ผ ๋ฐ CI/CD
- 4. ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์์ด Canvas ๊ตฌํํ๊ธฐ
- 5. ์บ๋ฒ์ค ๋๊ธฐํ๋ฅผ ์ํ ์์ CRDT ๊ตฌํ๊ธฐ
-
6. ์ปดํฌ๋ํธ ํจํด๋ถํฐ ์น์์ผ๊น์ง, ํจ์จ์ ์ธ FE ์ค๊ณ
- ์ข์ ์ปดํฌ๋ํธ๋ ๋ฌด์์ธ๊ฐ? + Headless Pattern
- ํจ์จ์ ์ธ UI ์ปดํฌ๋ํธ ์คํ์ผ๋ง: Tailwind CSS + cn.ts
- Tailwind CSS๋ก ๋์์ธ ์์คํ ๋ฐ UI ์ปดํฌ๋ํธ ์ธํ
- ์น์์ผ ํด๋ผ์ด์ธํธ ๊ตฌํ๊ธฐ: React ํ๊ฒฝ์์ ํจ์จ์ ์ธ ์น์์ผ ์ํคํ ์ฒ
- ์น์์ผ ํด๋ผ์ด์ธํธ ์ฝ๋ ๋ถ์ ๋ฐ ๊ณต์
- 7. ํธ๋ฌ๋ธ ์ํ ๋ฐ ์ฑ๋ฅ/UX ๊ฐ์
- 1์ฃผ์ฐจ ๊ธฐ์ ๊ณต์
- 2์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 3์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 4์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- 5์ฃผ์ฐจ ๋ฐ๋ชจ ๋ฐ์ด
- WEEK 06 ์ฃผ๊ฐ ๊ณํ
- WEEK 06 ๋ฐ์ผ๋ฆฌ ์คํฌ๋ผ
- WEEK 06 ์ฃผ๊ฐ ํ๊ณ