claude/rules 활용

|

.claude/rules/ 디렉토리

rules 디렉토리는 경로 기반 컨텍스트 주입 기능을 제공
CLAUDE.md는 프로젝트 전체에 적용되는 설정
rules는 특정 파일이나 디렉토리에서 작업할 때만 자동 활성화 됨

.claude/rules/api-route.md 예시

아래 규칙은 /src/network/api 아래의 TypeScript 파일 작성시만 적용됨
작업시 필요한 규칙만 로딩되어, 효율적으로 토큰을 관리할 수 있음

---
paths:
  - "src/network/api/**/*.ts"
---

# API 개발 규칙

## 응답 형식

모든 API는 다음 형식으로 응답한다.

{
"success": boolean,
"data": any | null,
"error": string | null
}

## 에러 처리

- 비즈니스 로직 에러: 500번대
- 서버 에러: 500번대
- 클라이언트 에러: 400번대
- 모든 에러는 AppError 클래스로 래핑

## 인증

- 인증 필요 라우트는 authMiddleware 적용 필수
- 권한 체크는 checkPermission 미들웨어 사용

## 유효성 검사

- 모든 입력은 zod 스키마로 검증
- 스키마 파일은 src/schemas/에 위치

CLAUDE.md로 프로젝트 설정하기

|

CLAUDE.md

/init 명령어로 CLAUDE.md 파일을 생성할 수 있음
프로젝트의 구조, 코딩 컨벤션, 배포 방법 등을 설명하는 문서라고 볼 수 있음

유형 위치 용도 공유 범위
프로젝트 디렉토리 [프로젝트]/CLAUDE.md
[프로젝트]/.claude/CLAUDE.md
팀 공유 프로젝트 설정
사용자 디렉토리 ~/.claude/CLAUDE.md 개인 전역 설정 개인
프로젝트 로컬 [프로젝트]/CLAUDE.local.md 개인 프로젝트 설정 개인(.gitignore 활용)

사용자 디렉토리(~/.claude/)

사용자 메모리 설정인 ~/.claude/는 개인의 코딩 스타일, 선호 도구, 공통 명령어 등을 저장할 수 있음

프로젝트 디렉토리([프로젝트]/.claude)

팀 차원에서 공유하려면 프로젝트 메모리를 활용 아래와 같은 형태로 다양한 역할의 파일들을 구성할 수 있음

.
└── .claude
    ├── CLAUDE.md
    ├── settings.json
    ├── agents
    │   └── todo-crud.md
    ├── commands
    │   ├── deploy.md
    │   └── migrate.md
    └── rules
        ├── api-rule.md
        └── component-rule.md

파일 역할
CLAUDE.md 프로젝트 개요, 도메인 모델, API 명세, 코딩 컨벤션, 네이밍 규칙, 금지사항 등
settings.json TypeScript 설정, lint 규칙, 설정 등
commands.deploy.md 배포 명령어
commands/migrate.md 마이그레이션 명령어
agents/todo-crud.md TODO CRUD 구현 에이전트
rules/api-rule.md API 가이드 룰
rules/component-rule.md 컴포넌트 가이드 룰

파일 로딩 시점

  • CLAUDE.md는 세션이 시작될 때 자동 로딩
  • .claude/rules/*.md 파일들은 해당 경로의 파일 작업시에만 조건부 로딩
  • 나중에 로드된 설정이 이전 설정을 덮어 씀
  • 숫자가 클수록 우선 순위가 높음

효과적인 CLAUDE.md

아래 정보를 포함하는 것이 좋음

  • 프로젝트 개요: 한 두 문장의 프로젝트 설명, 주요 기능 및 목적
  • 기술 스택: 사용 언어, 프레임워크 및 주요 라이브러리
  • 핵심 명령어: 빌드, 테스트, lint 등 명령어
  • 디렉토리 구조
  • 코딩 규칙: 네이밍 컨벤션, 스타일 가이드
  • 금지 사항과 예외 규칙: 파일 수정, 삭제 관련 규칙, git, db 등 민감한 조작 금지 등

CLAUDE.md가 너무 길어지면 좋지 않고 200라인 이내가 좋음
너무 길어질 경우 @import를 사용해서 작성하는 것이 좋음

# CLAUDE.md

프로젝트 개요

## 상세 문서

- API 설계: @docs/api-spec.md
- Git workflow: @docs/git-workflow.md

MacOS에서 Kubespray 클러스터에 kubectl로 접속하기

|

kubectl 설치


# kubectl cli 설치
$ brew install kubernetes-cli

VM에서 설정 파일(kubeconfig) 가져오기

쿠버네티스 클러스터에 접속하기 위한 인증 정보인 admin.conf 파일을 VM에서 맥북으로 가져와야 함

# 맥북의 ~/.kube 디렉토리 생성
mkdir -p ~/.kube

# Multipass VM 내부의 설정 파일을 맥북으로 복사
# VM 내부로 접속
multipass shell k8s-node

# 파일을 ubuntu 홈 디렉토리로 복사하고 소유권 변경
sudo cp /etc/kubernetes/admin.conf ~/admin.conf
sudo chown ubuntu:ubuntu ~/admin.conf

# VM 접속 종료
exit

# 맥 터미널에서 아래 명령어 실행(아마 오류)
multipass transfer k8s-node:~/admin.conf ~/.kube/config.tmp

# 맥에서 위 ~ 을 해석하지 못하기 때문에 아래 코드 실행
multipass transfer k8s-node:/home/ubuntu/admin.conf ~/.kube/config.tmp

IP 주소 업데이트 및 설정 적용

# VM의 IP를 자동으로 가져와서 127.0.0.1 부분을 수정합니다.
VM_IP=$(multipass info k8s-node | grep IPv4 | awk '{print $2}')
sed "s/127.0.0.1/$VM_IP/g" ~/.kube/config.tmp > ~/.kube/config

# 권한 설정
chmod 600 ~/.kube/config
rm ~/.kube/config.tmp

클러스터 확인

$ kubectl get nodes

NAME    STATUS   ROLES           AGE   VERSION
node1   Ready    control-plane   28m   v1.35.1
$ kubectl get pods -A

NAMESPACE     NAME                                      READY   STATUS    RESTARTS   AGE
kube-system   calico-kube-controllers-5bc89bc76-w6rpm   1/1     Running   0          63m
kube-system   calico-node-5p2vf                         1/1     Running   0          63m
kube-system   coredns-58cc5d8ddf-f877q                  1/1     Running   0          63m
kube-system   dns-autoscaler-7ccf68df7c-m75nw           1/1     Running   0          63m
kube-system   kube-apiserver-node1                      1/1     Running   0          64m
kube-system   kube-controller-manager-node1             1/1     Running   0          64m
kube-system   kube-proxy-qzdzj                          1/1     Running   0          64m
kube-system   kube-scheduler-node1                      1/1     Running   0          64m
kube-system   nodelocaldns-b4t9q                        1/1     Running   0          63m

MacOS에서 Kubespray로 클러스터구축하기

|

사전 준비

  • Multipass: macOS에서 우분투 VM을 가장 쉽게 구동하게 해주는 도구
  • Ansible: Kubespray 실행을 위해 로컬(호스트)에 설치가 필요
# Multipass 설치
$ brew install --cask multipass

# kubectl cli 설치
$ brew install kubernetes-cli

가상 머신(VM) 생성

싱글 노드 클러스터를 위해 최소 2개 이상의 CPU와 4GB 이상의 RAM을 가진 VM을 생성 필요

# 'k8s-node'라는 이름의 VM 생성 (Ubuntu 22.04/24.04 추천)
$ multipass launch --name k8s-node --cpus 2 --memory 4G --disk 20G

# 생성된 VM의 IP 확인
$ multipass info k8s-node

Name:           k8s-node
State:          Running
Snapshots:      0
IPv4:           192.168.64.2
Release:        Ubuntu 24.04.4 LTS
Image hash:     99e1d482b958 (Ubuntu 24.04 LTS)
CPU(s):         2
Load:           0.24 0.08 0.02
Disk usage:     2.1GiB out of 19.3GiB
Memory usage:   293.3MiB out of 3.8GiB
Mounts:         --

# 아래 명령어로도 가능
$ multipass list

Name                    State             IPv4             Image
k8s-node                Running           192.168.64.2     Ubuntu 24.04 LTS

위에서 192.168.64.2를 기억해야 함

접속 테스트

$ multipass exec k8s-node -- bash

만약 exec failed: ssh connection failed: 'Failed to connect: No route to host'와 같은 오류가 발생하면, 아래 명령어 실행

ssh-keygen -R 192.168.64.2
ssh-keygen -R k8s-node

SSH 접속 설정

Kubespray(Ansible)가 VM에 접속할 수 있도록 SSH 키를 복사

# VM으로 공개키 복사 (사용자명은 기본적으로 'ubuntu')
$ multipass exec k8s-node -- bash -c "mkdir -p ~/.ssh && echo '$(cat ~/.ssh/id_ed25519.pub)' >> ~/.ssh/authorized_keys"

# 비밀번호 없이 sudo 가능하도록 설정 (선택 사항이나 편리함)
$ multipass exec k8s-node -- bash -c "echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' | sudo tee /etc/sudoers.d/ubuntu"

Kubespray 설정 및 배포

# Kubespray 소스 다운로드
git clone https://github.com/kubernetes-sigs/kubespray.git
cd kubespray

# 가상환경 설정
python -m venv venv
source venv/bin/activate

# 의존성 설치 (가상환경 권장)
# Python는 3.12 버전 이상
pip install --upgrade ansible
pip3 install -r requirements.txt

# 설정 복사
cp -rfp inventory/sample inventory/snowdeer-cluster

인벤토리 수정 (inventory/snowdeer-cluster/inventory.ini)

싱글 노드이므로 모든 역할을 하나의 IP에 할당.
[all] 섹션에 VM IP를 적고, 나머지 그룹에도 동일한 호스트명을 입력

[all]
node1 ansible_host=192.168.64.2 ip=192.168.64.2 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_ed25519

[kube_control_plane]
node1

[etcd]
node1

[kube_node]
node1

[k8s_cluster:children]
kube_control_plane
kube_node

클러스터 설정 변경

싱글 노드에서는 리소스 최적화를 위해 일부 설정을 변경하는 것이 좋음.
inventory/snowdeer-cluster/group_vars/k8s_cluster/k8s-cluster.yml에서 다음 설정 확인

kube_network_plugin: calico

플레이북 실행 (설치 시작)

10~20분 이상 시간이 걸리기도 함

ansible-playbook -i inventory/snowdeer-cluster/inventory.ini --become --become-user=root cluster.yml

클러스터 확인

설치가 완료되면 VM에 접속하여 상태를 확인

multipass shell k8s-node

# kubectl 설치
sudo snap install kubectl --classic

# 쿠베컨피그 설정 (root 권한 필요)
sudo cp /etc/kubernetes/admin.conf ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config

# 노드 상태 확인
kubectl get nodes

Python Login 백엔드 샘플 2(Poetry, FastAPI, PyTest)

|

snowdeer-user-api

샘플 프로젝트 생성

$ poetry new snowdeer-user-api

Created package snowdeer_user_api in snowdeer-user-api

아래와 같은 폴더 구조가 생성됨

.
├── pyproject.toml
├── README.md
├── snowdeer_user_api
│   └── __init__.py
└── tests
    └── __init__.py

패키지 설치

$ cd snowdeer-user-api
$ poetry add "fastapi[standard]" uvicorn
$ poetry add --group dev ruff pytest

샘플 코드 작성

snowdeer_user_api/main.py

from fastapi import FastAPI

app = FastAPI(title="snowdeer's User API")

@app.get("/hello")
async def hello():
    return {"message": "Hello, snowdeer!"}
# 아래 명령어로 실행
$ poetry run uvicorn snowdeer_user_api.main:app --reload

# 다른 터미널에서 확인
$ curl localhost:8000/hello

{"message":"Hello Snowdeer Users API!"}

SQLAlchemy 활용 Users 테이블 생성

$ poetry add sqlalchemy pydantic-settings

snowdeer_user_api/database.py

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./users.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    password = Column(String)

snowdeer_user_api/main.py

from fastapi import FastAPI
from .database import engine, Base, SessionLocal

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

app = FastAPI(title="snowdeer's User API")

@app.get("/hello")
async def hello():
    return {"message": "Hello, snowdeer!"}

회원가입(Register) 및 로그인(Login) 엔드포인트 추가

snowdeer_user_api/schemas.py

from pydantic import BaseModel

class UserCreate(BaseModel):
    username: str
    password: str

class UserLogin(BaseModel):
    username: str
    password: str

snowdeer_user_api/database.py

from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

SQLALCHEMY_DATABASE_URL = "sqlite:///./users.db"
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    id = Column(Integer, primary_key=True, index=True)
    username = Column(String, unique=True, index=True)
    password = Column(String)

Base.metadata.create_all(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

snowdeer_user_api/main.py

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from .schemas import UserCreate, UserLogin
from .database import get_db, User


app = FastAPI(title="snowdeer's User API")

@app.get("/hello")
async def hello():
    return {"message": "Hello, snowdeer!"}

@app.post("/register")
async def register(user: UserCreate, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.username == user.username).first()
    if db_user:
        raise HTTPException(400, "Username already exists")
    new_user = User(username=user.username, password=user.password)
    db.add(new_user)
    db.commit()
    return {"msg": "User created"}

@app.post("/login")
async def login(user: UserLogin, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.username == user.username).first()
    if not db_user or db_user.password != user.password:
        raise HTTPException(401, "Invalid credentials")
    return {"msg": "Login successful"}

아래 명령어로 동작 확인

# 아직 회원 가입을 안했기 때문에 오류가 생겨야 정상
$ curl -X POST http://localhost:8000/login \
  -H "Content-Type: application/json" \
  -d '{"username": "snowdeer", "password": "snowdeer"}'

{"detail":"Invalid credentials"}
# 회원 가입
curl -X POST http://localhost:8000/register \
  -H "Content-Type: application/json" \
  -d '{"username": "snowdeer", "password": "snowdeer"}'

{"msg":"User created","user_id":1}
# 로그인 테스트
$ curl -X POST http://localhost:8000/login \
  -H "Content-Type: application/json" \
  -d '{"username": "snowdeer", "password": "snowdeer"}'

{"msg":"Login successful"}

# 잘못된 패스워드로 테스트
$ curl -X POST http://localhost:8000/login \
  -H "Content-Type: application/json" \
  -d '{"username": "snowdeer", "password": "wrong-password"}'

{"detail":"Invalid credentials"}

SHA256 암호화 적용

snowdeer_user_api/utils.py

import hashlib

def hash_password(password: str) -> str:
    return hashlib.sha256(password.encode()).hexdigest()

snowdeer_user_api/main.py

아래 부분을 수정

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from .schemas import UserCreate, UserLogin
from .database import get_db, User
from .utils import hash_password


app = FastAPI(title="snowdeer's User API")

@app.get("/hello")
async def hello():
    return {"message": "Hello, snowdeer!"}

@app.post("/register")
async def register(user: UserCreate, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.username == user.username).first()
    if db_user:
        raise HTTPException(400, "Username already exists")
    new_user = User(username=user.username, password=hash_password(user.password))
    db.add(new_user)
    db.commit()
    return {"msg": "User created"}

@app.post("/login")
async def login(user: UserLogin, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.username == user.username).first()
    if not db_user or db_user.password != hash_password(user.password):
        raise HTTPException(401, "Invalid credentials")
    return {"msg": "Login successful"}

암호화에 Salt 적용

snowdeer_user_api/utils.py

import hashlib
import secrets

def generate_salt() -> str:
    return secrets.token_hex(16)

def hash_password(password: str, salt: str = None) -> tuple[str, str]:
    if not salt:
        salt = generate_salt()
    salted_password = f"{password}{salt}"
    hashed = hashlib.sha256(salted_password.encode()).hexdigest()
    return hashed, salt

snowdeer_user_api/main.py

from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
from .schemas import UserCreate, UserLogin
from .database import get_db, User
from .utils import hash_password


app = FastAPI(title="snowdeer's User API")

@app.get("/hello")
async def hello():
    return {"message": "Hello, snowdeer!"}

@app.post("/register")
async def register(user: UserCreate, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.username == user.username).first()
    if db_user:
        raise HTTPException(400, "Username already exists")
    
    hashed, salt = hash_password(user.password)
    new_user = User(username=user.username, password=f"{hashed}:{salt}")
    db.add(new_user)
    db.commit()
    return {"msg": "User created"}

@app.post("/login")
async def login(user: UserLogin, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.username == user.username).first()
    
    if not db_user:
        raise HTTPException(401, "Invalid credentials")
    
    _, salt = db_user.password.split(":")
    hashed_input, _ = hash_password(user.password, salt)
    if hashed_input != db_user.password.split(":")[0]:
        raise HTTPException(401, "Invalid credentials")
    
    return {"msg": "Login successful"}