AI를 활용한 개발
코드를 쓰는 도구로서의 AI와, 제품 기능으로서의 AI는 다른 이야기입니다. 둘 다 프론트엔드 작업에 들어온 지 오래되었으니 순서대로 봅니다.
LLM의 동작 방식과 기존 코딩 방식과의 차이
LLM은 주어진 문맥을 바탕으로 다음에 올 내용을 확률적으로 이어 씁니다. 여기서 평소 쓰던 도구와의 차이가 전부 나옵니다.
| 컴파일러·린터 | LLM | |
|---|---|---|
| 같은 입력 | 항상 같은 출력 | 다를 수 있음 |
| 모르는 것 | 오류를 냅니다 | 그럴듯하게 지어냅니다 |
| 근거 | 규칙 | 학습한 분포 |
| 아는 범위 | 지금 이 프로젝트 | 학습 시점까지의 일반적인 코드 |
실무에서 체감하는 결과는 이렇습니다.
- 존재하지 않는 API나 옵션을 자신 있게 씁니다
- 최신 버전에서 바뀐 문법을 예전 방식으로 씁니다
- 프로젝트의 규칙은 알려 주지 않으면 모릅니다
그래서 맞는지 확인하는 비용이 작업의 일부로 들어옵니다. 이 비용을 줄이는 쪽으로 쓰는 것이 요령입니다. 검증이 쉬운 일(테스트가 있는 코드, 타입이 잡아 주는 코드, 눈으로 바로 보이는 UI)에 먼저 쓰고, 조용히 틀릴 수 있는 일에는 조심해서 씁니다.
AI 보조 코딩 도구
| 도구 | 형태 |
|---|---|
| Claude Code | 터미널에서 파일을 직접 읽고 고칩니다 |
| Cursor | 편집기 안에서 프로젝트 전체를 문맥으로 씁니다 |
| GitHub Copilot | 편집기에서 다음 줄을 제안합니다 |
도구마다 문맥을 어디까지 보는지가 다릅니다. 열린 파일만 보는 도구에 "이 프로젝트 규칙대로"라고 말해도 소용이 없습니다. 무엇을 읽을 수 있는지 먼저 확인하고 그에 맞게 지시합니다.
프로젝트 규칙을 파일로 적어 두면 매번 설명하지 않아도 됩니다.
<!-- CLAUDE.md -->
- 스타일은 CSS 모듈을 씁니다. 인라인 스타일은 쓰지 않습니다.
- API 응답은 반드시 스키마로 검증한 뒤 컴포넌트에 넘깁니다.
- 테스트는 Vitest로 작성하고, 파일 옆에 둡니다.의도와 제약을 분명히 전달하는 프롬프트 작성
잘 안 되는 요청은 대개 정보가 빠진 요청입니다.
로그인 폼 만들어 줘무엇으로, 어떤 규칙으로, 실패하면 어떻게 할지가 없으니 전부 추측으로 채워집니다. 네 가지를 넣으면 달라집니다.
- 목표 — 무엇을 만드는가
- 맥락 — 어떤 코드 위에 올라가는가
- 제약 — 쓰면 안 되는 것, 지켜야 하는 것
- 완료 기준 — 무엇이 되면 끝인가
기존 useForm 훅을 써서 로그인 폼을 만들어 줘.
- 이메일과 비밀번호, 둘 다 필수
- 오류 메시지는 입력 아래에 두고 aria-describedby로 연결
- 제출 중에는 버튼 비활성화
- 새 라이브러리는 추가하지 말 것
완료 기준: 키보드만으로 제출까지 가능하고, Vitest 테스트가 통과할 것긴 작업은 한 번에 시키지 않고 나눕니다. 먼저 계획을 받아 확인하고, 그다음에 코드를 받습니다. 중간에 방향이 틀어졌을 때 되돌릴 양이 적어집니다.
코드 리뷰, 리팩터링, 문서 생성에 활용하기
코드를 새로 쓰는 것보다 이미 있는 코드를 다루는 일에서 더 안정적입니다. 정답이 코드 안에 있기 때문입니다.
리뷰는 관점을 지정할 때 쓸 만합니다.
이 컴포넌트를 접근성 관점에서만 봐 줘.
키보드 조작, 포커스 순서, 스크린 리더에 전달되는 이름 세 가지.범위를 좁히지 않으면 "변수명을 바꾸세요" 같은 말이 섞여 들어옵니다.
리팩터링은 동작이 바뀌지 않았다는 것을 확인할 수단이 있을 때만 맡깁니다.
1. 먼저 지금 동작을 그대로 검증하는 테스트를 작성한다
2. 테스트가 통과하는 것을 확인한다
3. 그다음에 구조를 바꾼다문서 생성은 가장 손이 덜 가는 쪽입니다. 다만 "왜 이렇게 했는지"는 코드에 없으므로 지어내게 둬서는 안 됩니다. 사람이 채웁니다.
에이전트, MCP와 스킬로 작업 환경 확장하기
대화로 코드를 받아 붙여 넣는 단계에서, 도구가 직접 파일을 읽고 명령을 실행하는 단계로 넘어왔습니다.
- 에이전트 — 목표를 주면 파일 읽기, 수정, 테스트 실행을 스스로 반복합니다
- MCP — 외부 시스템(이슈 트래커, 데이터베이스, 디자인 도구)을 도구로 연결하는 규약입니다
- 스킬 — 특정 작업의 절차와 규칙을 묶어 두고, 필요할 때 불러 쓰게 만든 것입니다
예를 들어 이슈 트래커를 MCP로 연결하면 티켓 내용을 사람이 옮겨 붙이지 않아도 됩니다.
주의할 점은 권한입니다. 파일을 고치고 명령을 실행할 수 있다는 것은 되돌릴 수 없는 일도 할 수 있다는 뜻입니다.
- 작업은 별도 브랜치에서 하고, 커밋 전에 diff를 직접 봅니다
- 외부로 나가는 동작(배포, 푸시, 메시지 전송)은 확인을 거칩니다
- 자동으로 허용할 명령의 범위를 좁게 잡습니다
LLM API로 제품에 기능 붙이기
요약, 분류, 추출 같은 기능을 직접 넣을 때입니다. 프론트엔드에서 가장 먼저 정할 것은 API 키를 어디에 두는가입니다.
// 브라우저에서 직접 호출 — 절대 하지 않습니다
const client = new Anthropic({ apiKey: "sk-ant-..." });번들에 들어간 키는 개발자 도구만 열면 보입니다. 누구나 당신의 계정으로 요청을 보낼 수 있게 됩니다. 호출은 서버에서 합니다.
브라우저 → 내 서버(키 보관, 사용량 제한) → LLM API서버 쪽 코드입니다.
// server/summary.ts
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // ANTHROPIC_API_KEY를 환경 변수에서 읽습니다
const response = await client.messages.create({
model: "claude-opus-5-5",
max_tokens: 16000,
system: "상품 설명을 한국어 세 문장으로 요약합니다. 없는 사실을 덧붙이지 않습니다.",
messages: [{ role: "user", content: description }],
});
const text = response.content.find((block) => block.type === "text")?.text ?? "";응답은 수 초가 걸립니다. 다 기다렸다 한 번에 보여 주면 멈춘 것처럼 보이므로 스트리밍으로 내보냅니다.
const stream = client.messages.stream({
model: "claude-opus-5-5",
max_tokens: 16000,
messages: [{ role: "user", content: description }],
});
stream.on("text", (delta) => controller.enqueue(encoder.encode(delta)));
await stream.finalMessage();브라우저에서는 응답 본문을 조각으로 읽습니다.
const response = await fetch("/api/summary", { method: "POST", body });
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
while (true) {
const { value, done } = await reader.read();
if (done) break;
output.textContent += value;
}화면 쪽에서 챙길 것은 평소 비동기 처리와 같습니다. 로딩 표시, 중간 취소, 실패 시 재시도, 그리고 빈 응답과 이상한 응답입니다. 모델 출력은 외부 입력이므로 그대로 innerHTML에 넣지 않습니다. 사용량 제한과 비용 상한도 서버에서 겁니다. 반복 호출 한 번이면 요금이 올라갑니다.
Anthropic 외에 OpenAI, Gemini도 같은 구조입니다. 바뀌는 것은 SDK와 모델 이름뿐이고, 키를 서버에 두고 스트리밍으로 내려보내는 틀은 같습니다.
생성된 코드를 그대로 믿지 않고 검증하는 습관
틀린 코드보다 위험한 것은 그럴듯하게 동작하는 코드입니다. 화면은 멀쩡한데 접근성이 빠져 있거나, 동작은 하는데 요청을 취소하지 않아 오래된 응답이 덮어쓰는 식입니다.
읽고 넘기기 전에 보는 자리들입니다.
- 실제로 존재하는 API인지 — 문서에서 확인합니다
- 로딩, 빈 결과, 실패 상태가 들어 있는지
- 사용자 입력이 HTML이나 URL로 들어가는 자리가 있는지
- 키보드로 조작되는지, 레이블이 연결되어 있는지
- 리스너와 타이머를 정리하는지
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal }).then(/* ... */);
return () => controller.abort(); // 빠져 있는 경우가 잦습니다
}, [url]);가장 확실한 방법은 자동 검사입니다. 타입 검사, 린터, 테스트가 걸려 있으면 사람이 읽기 전에 상당수가 걸러집니다. 반대로 이런 장치가 없는 프로젝트일수록 AI가 만든 코드를 받아들이는 비용이 큽니다.
설명할 수 없는 코드는 머지하지 않습니다. 이 기준 하나만 지켜도 대부분의 문제는 들어오지 않습니다.