오늘은 Claude Code와 블로그를 손보면서 세 가지를 했다.
- CLAUDE.md 작성: 이 폴더에서 물어볼 때마다 코드를 다시 읽느라 토큰을 쓰지 않게 한다.
- 검수(checker) 규칙: 테스트 프레임워크를 붙이고, 검사가 통과해야 작업이 끝난 것으로 본다.
- 메인 목록 페이지 나누기: PC는 페이지 번호, 모바일은 더보기로 만든다.
1. CLAUDE.md: 매번 같은 걸 다시 읽지 않게
Claude Code는 프로젝트 루트의 CLAUDE.md를 세션이 시작될 때마다 읽는다. 여기에 프로젝트 구조와 규칙을 적어 두면 질문할 때마다 파일을 뒤져 가며 파악하는 과정이 줄어든다. /init으로 코드를 분석해 만들고, 필요한 것만 남겼다.
넣은 내용은 이렇다.
- 명령어:
dev,check,build,db:push. 배포는 git push가 아니라vercel deploy --prod다. - 읽지 말 것:
static/reports/(46MB), 자동 생성되는reports.json,local.db,.svelte-kit/. 실수로 이걸 열면 토큰이 크게 나간다. - 여러 파일을 읽어야 알 수 있는 구조:
- DB는 로컬 SQLite와 Turso를 환경변수로 자동 전환한다. 컬럼을 추가하면
schema.ts와db.ts의 로컬 DDL 두 곳을 같이 고쳐야 한다. - 관리자 확인은 라우트 그룹이 아니라 기능마다
if (!locals.isAdmin)로 직접 한다. - 공개 페이지는 CDN에 1시간 캐시되고, POST 요청이 성공하면 캐시를 지운다.
- 댓글·방명록은
{@html}로 출력하지 않는다.
- DB는 로컬 SQLite와 Turso를 환경변수로 자동 전환한다. 컬럼을 추가하면
- 블로그 글 규칙: 제목, 본문, 슬러그 모두 한국어로 쓴다.
모든 파일을 나열하는 대신, 파일 하나만 봐서는 모르는 규칙을 적는 게 핵심이다. 파일 목록은 Claude가 직접 찾을 수 있다.
2. 검수 규칙: npm run verify가 통과해야 끝
이 블로그에는 테스트가 하나도 없었다. 검증 수단은 svelte-check 타입 검사뿐이었다. 코드를 고칠 때마다 실패를 줄이려고 테스트 프레임워크를 붙였다.
SvelteKit은 Vite 기반이라 Vitest가 가장 자연스럽다. 설정 파일을 따로 만들 필요 없이 vite.config.ts에 한 줄이면 된다.
// vite.config.ts
export default defineConfig({
plugins: [sveltekit()],
test: {
include: ['src/**/*.test.ts']
}
});
SvelteKit 플러그인이 그대로 붙어 있어서 $lib나 $env/dynamic/private를 import하는 코드도 테스트에서 그대로 돌아간다.
그리고 타입 검사, 테스트, 빌드를 한 번에 돌리는 명령을 만들었다.
"test": "vitest run",
"verify": "npm run check && npm run test && npm run build"
&&로 이었기 때문에 하나라도 실패하면 거기서 멈춘다.
첫 테스트는 DB 없이 돌아가는 순수 함수부터 골랐다. 깨지면 바로 사고가 나는 곳이다.
- 예약 발행: 미래 시각으로 예약한 글이 미리 보이지 않는지, 한국 시간(KST) 변환이 맞는지
- 마크다운 살균:
<script>,onerror,javascript:링크가 걸러지는지 - 슬러그: 한글 제목이
sveltekit-블로그-개발-기록처럼 바뀌는지
마지막으로 CLAUDE.md에 규칙을 적었다.
npm run verify가 0으로 끝나야 작업 완료다.- 실패하면 원인을 고친다. 테스트를 지워서 통과시키지 않는다.
src/lib의 로직을 바꾸면 옆에*.test.ts를 추가한다.- 보고할 때는
Tests 14 passed,0 ERRORS같은 실제 결과 줄을 인용한다.
마지막 규칙이 생각보다 중요하다. AI가 "통과했습니다"라고만 말하면 정말 돌렸는지 알 수 없다. 결과 줄을 보여 달라고 하면 돌리지 않고는 답할 수 없다.
3. 메인 목록: PC는 페이지 번호, 모바일은 더보기
메인 화면이 글을 최대 50개까지 한 번에 보여 줘서 너무 길었다. 한 페이지에 10개씩 나눴다.
주소로 상태를 들고 다닌다
PC와 모바일을 따로 만들지 않고 같은 주소 규칙 하나로 처리했다.
| 주소 | 보여 주는 글 |
|---|---|
/ |
1~10번 |
/?page=2 |
11~20번 (PC 번호) |
/?from=1&page=2 |
1~20번 (모바일 더보기) |
from부터 page까지를 이어서 보여 주는 방식이다. 모바일 더보기는 /?from=1&page=3 같은 링크일 뿐이다. 그래서 새로고침하거나 뒤로 가도 보던 목록이 남고, 자바스크립트가 없어도 동작한다. SvelteKit의 data-sveltekit-noscroll을 링크에 붙이면 페이지를 넘겨도 스크롤 위치가 그대로라서 아래에 이어 붙는 것처럼 보인다.
주소 값을 DB 조회 범위로 바꾸는 부분은 작은 함수로 뺐다.
export function pageWindow(params: URLSearchParams, total: number, size = POSTS_PER_PAGE): PageWindow {
const totalPages = Math.max(1, Math.ceil(total / size));
const page = Math.min(positiveInt(params.get('page')) ?? 1, totalPages);
const from = Math.min(positiveInt(params.get('from')) ?? page, page);
return { page, from, totalPages, offset: (from - 1) * size, limit: (page - from + 1) * size };
}
?page=abc나 ?page=99 같은 잘못된 값은 가장 가까운 정상 페이지로 바꾼다. 이 함수에도 테스트 5개를 붙였다. 방금 만든 검수 규칙을 바로 써먹은 셈이다.
100개 읽고 거르기에서 10개만 가져오기로
전에는 글 100개를 통째로 읽은 뒤 자바스크립트로 예약 글을 거르고 50개를 잘랐다. 이제는 공개 조건을 SQL로 옮기고 필요한 10개만 가져온다.
// 발행됨 + 예약 시각이 없거나 지남
const visible = and(
eq(posts.published, true),
or(isNull(posts.publishAt), lte(posts.publishAt, nowIso()))
);
전체 글 수는 count()로 따로 센다. 본문(content)도 목록에 필요 없어서 조회 컬럼에서 뺐다.
PC와 모바일 나누기는 CSS로
페이지 번호와 더보기 버튼을 둘 다 그려 두고, 화면 폭 760px를 기준으로 하나만 보여 준다. 헤더 메뉴가 쓰는 기준과 같다.
.pager-more { display: none; }
@media (max-width: 760px) {
.pager-pages { display: none; }
.pager-more { display: inline-flex; width: 100%; }
}
더보기로 붙는 글은 등장 애니메이션 지연이 10번째부터 길어지는 문제가 있었다. 글 번호를 페이지 크기로 나눈 나머지(i % 10)로 지연을 계산해서, 새로 붙는 10개도 처음부터 차례로 나타나게 했다.
결과
배포 전에 검수를 돌렸다.
0 ERRORS
Test Files 4 passed (4)
Tests 19 passed (19)
✔ done
운영 사이트에서도 /는 10개, ?page=2는 다음 10개, ?from=1&page=2는 20개가 나오는 것을 확인했다.
남은 것
- 테스트는 아직 순수 함수만 다룬다. 폼 처리, DB 조회, 실제 브라우저 동작(E2E)은 검사하지 않는다.
- 검수 규칙은
CLAUDE.md에 적은 약속이라 강제되지는 않는다. 작업이 끝날 때 자동으로 돌게 하려면 Claude Code의 Stop hook을 걸면 된다. - 페이지 번호를 전부 나열하고 있어서, 페이지가 20개를 넘으면 중간을
…로 줄이는 방식으로 바꿔야 한다.
댓글 0개
아직 댓글이 없습니다. 첫 댓글을 남겨보세요.