
Prompt Engineering: 개발자가 알아야 할 프롬프트 작성법
AI에게 '로그인 만들어줘'라고 했더니 엉망인 코드가 나왔다. 프롬프트를 구조화하니 결과물이 완전히 달라졌다.

AI에게 '로그인 만들어줘'라고 했더니 엉망인 코드가 나왔다. 프롬프트를 구조화하니 결과물이 완전히 달라졌다.
둘 다 같은 Transformer 자식인데 왜 다를까? '빈칸 채우기'와 '이어 쓰기' 비유로 알아보는 BERT와 GPT의 결정적 차이. 프로젝트에서 겪은 시행착오와 선택 가이드.

ChatGPT는 질문에 답하지만, AI Agent는 스스로 계획하고 도구를 사용해 작업을 완료한다. 이 차이가 왜 중요한지 정리했다.

ESLint와 Prettier 설정 충돌로 삽질한 경험, 누구나 있을 것이다. Biome는 이 둘을 하나로 합치고 속도까지 잡았다.

세 가지 AI 코딩 도구를 실제로 써보고 비교했다. 자동완성, 코드 생성, 리팩토링 각각 어디가 강한지 솔직한 후기.

작년까지만 해도 나는 AI를 검색엔진처럼 썼다. "React 로그인 페이지 만들어줘"라고 던지면 뭐라도 나올 거라고 생각했다. 결과는? class component에 inline style 범벅인 2018년스러운 코드. 쓸 수가 없었다.
문제는 AI가 아니라 내 프롬프트였다. AI는 레스토랑 주방장이 아니다. "뭔가 맛있는 거"라고 주문하면 주방장도 당황한다. 메뉴판을 보고, 재료를 말하고, 조리법을 설명해야 원하는 요리가 나온다. AI도 똑같다.
프롬프트를 구조화하니까 결과물이 완전히 달라졌다. "로그인 만들어줘" 대신 역할, 컨텍스트, 제약조건, 예시를 주었더니 production 코드가 나왔다. 이게 prompt engineering이었다. 코드 짜듯이 프롬프트를 짜면, AI는 내가 원하는 결과를 정확히 뱉어낸다.
개발자로서 API 문서 읽고, 함수 시그니처 맞추고, 타입 지정하는 건 당연하다. 그런데 AI한테는 대충 말해도 알아듣겠지 싶었다. 이게 큰 착각이었다. LLM은 확률 기반 모델이다. 애매한 입력에는 평균적인 출력을 준다. 평균적인 코드는 쓸모없다.
프롬프트를 함수처럼 설계하면 된다는 걸 깨달았다. 입력(컨텍스트), 처리(역할과 작업), 출력(형식과 제약)을 명확히 하면, AI는 그대로 실행한다. 디버깅하듯 프롬프트를 반복 수정하면, 점점 더 정확한 결과가 나온다.
결국 프롬프트 엔지니어링은 새로운 프로그래밍 언어를 배우는 게 아니다. 내가 원하는 걸 명확히 전달하는 커뮤니케이션 스킬이다. 그리고 개발자는 이미 이런 사고방식을 가지고 있다. 코드 리뷰할 때, PR 설명 쓸 때, 이슈 티켓 작성할 때 이미 구조화된 커뮤니케이션을 한다. 프롬프트도 똑같다.
처음 AI를 쓸 때 내 프롬프트는 이랬다.
나쁜 예시:React로 로그인 페이지 만들어줘
결과는? 300줄짜리 class component에 state management도 없고, validation도 엉성하고, accessibility는 생각도 안 한 코드. 이걸 고치느라 차라리 처음부터 쓰는 게 나았다.
프롬프트를 이렇게 바꿨다.
좋은 예시:역할: 당신은 Next.js 13 App Router와 TypeScript를 사용하는 시니어 프론트엔드 개발자입니다.
컨텍스트:
- 프로젝트: SaaS 대시보드의 인증 시스템
- 스택: Next.js 13 App Router, TypeScript, Tailwind CSS, React Hook Form, Zod
- 인증: Supabase Auth 사용
- 디자인: shadcn/ui 컴포넌트 사용
작업:
로그인 페이지 컴포넌트를 작성해주세요.
제약조건:
- Server Component와 Client Component 분리
- Form validation은 Zod schema 사용
- Error handling 포함 (네트워크 오류, 잘못된 자격증명)
- Loading state 처리
- Tailwind CSS만 사용, inline style 금지
- ARIA labels 포함한 접근성 고려
예시 코드 스타일:
typescript
const LoginForm = () => {
const form = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
// ...
}
같은 AI, 같은 모델인데 결과물이 완전히 달랐다. TypeScript 타입 정의부터, Zod validation schema, error boundary, loading state까지 모두 포함된 production-ready 코드가 나왔다.
차이는 명확하다. 첫 번째는 "뭐 만들어줘"고, 두 번째는 "이런 환경에서, 이런 제약으로, 이런 스타일로 만들어줘"다. AI는 마법사가 아니라 초급 개발자다. 정확한 요구사항을 주면 정확한 결과를 낸다.
LLM은 컨텍스트 윈도우 안에서만 작동한다. 사람으로 치면 단기 기억이다. GPT-4는 128k 토큰, Claude는 200k 토큰. 한글은 토큰 소비가 크니까 실제로는 더 적다.
이걸 이해하니까 프롬프트 작성법이 달라졌다. 관련 없는 정보는 빼고, 핵심만 넣어야 한다. 1000줄 코드 전부 붙여넣지 말고, 문제되는 함수와 주변 타입 정의만 넣는다.
// 나쁜 예: 전체 파일 붙여넣기 (500줄)
이 파일에서 버그 찾아줘
[전체 파일 코드]
// 좋은 예: 핵심만 추출
다음 함수에서 race condition이 발생합니다.
관련 코드:
- fetchUser 함수 (비동기)
- updateUserCache 함수
- 호출 위치: UserProfile 컴포넌트 useEffect
[해당 부분만 50줄]
기대 동작: 동시 요청 시 최신 응답만 반영
현재 문제: 이전 요청이 나중에 도착하면 덮어씀
컨텍스트 윈도우는 냉장고 같다. 재료를 다 우겨넣으면 찾기 어렵다. 필요한 재료만 정리해서 넣으면, 요리사(AI)가 빠르게 찾아서 쓴다.
몇 달 동안 시행착오 끝에 내가 정착한 템플릿이다. 복잡한 작업일수록 이 구조를 따른다.
AI한테 "너는 누구다"를 먼저 알려준다. 이게 전체 응답 톤과 방향을 결정한다.
역할: 당신은 10년 경력의 백엔드 엔지니어로, Node.js와 PostgreSQL 전문가입니다.
역할을 주면 AI는 그 관점에서 생각한다. "주니어 개발자 멘토"라고 하면 설명이 자세해지고, "코드 리뷰어"라고 하면 비판적 시각으로 본다.
현재 상황, 사용 중인 기술 스택, 프로젝트 배경을 준다. 이게 없으면 AI는 일반적인 답변만 한다.
컨텍스트:
- 프로젝트: 실시간 채팅 앱
- 스택: Next.js 14, Supabase Realtime, TypeScript
- 현재 문제: 동시 접속자 100명 넘으면 메시지 지연 발생
- 데이터베이스: PostgreSQL, messages 테이블 인덱스 없음
이 정보가 있으면 AI는 "인덱스 추가하세요"같은 범용 답 대신, Supabase 특성을 고려한 구체적 해결책을 준다.
정확히 뭘 원하는지 명시한다. 모호하면 AI도 모호하게 답한다.
작업:
messages 테이블 쿼리 성능을 개선하는 마이그레이션 SQL을 작성해주세요.
"성능 개선해줘" 대신 "마이그레이션 SQL 작성"이라고 하면, 실행 가능한 코드가 나온다.
하지 말아야 할 것, 지켜야 할 규칙을 명확히 한다. 이게 없으면 AI는 맘대로 한다.
제약조건:
- 기존 데이터 손실 없이 인덱스 추가
- CONCURRENTLY 옵션 사용 (production 중단 불가)
- created_at, room_id 컬럼에 composite index
- 외래 키 제약조건 유지
특히 "~하지 마라"를 명시하는 게 중요하다. "테이블 재생성 금지", "기존 API 변경 금지"처럼.
원하는 출력 형식, 코드 스타일을 예시로 보여준다. Few-shot learning이 여기서 작동한다.
예시 형식:
-- Migration: add_messages_index
-- Date: 2026-01-13
BEGIN;
CREATE INDEX CONCURRENTLY idx_messages_room_created
ON messages(room_id, created_at DESC);
COMMIT;
AI는 패턴 매칭에 강하다. 예시를 주면 정확히 그 형식으로 출력한다.
Zero-shot은 "이거 해줘"고 던지는 것. Few-shot은 "이렇게 하면 돼, 따라해"라고 예시를 주는 것. 코드 생성에서 few-shot이 훨씬 강력하다.
Zero-shot (약함):이 함수를 TypeScript로 변환해줘
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
}
결과는? any 타입 범벅.
다음 패턴으로 TypeScript 변환해주세요.
예시 1:
// Before
function getPost(id) {
return fetch(`/api/posts/${id}`).then(r => r.json())
}
// After
type Post = {
id: string;
title: string;
content: string;
};
async function getPost(id: string): Promise<Post> {
const response = await fetch(`/api/posts/${id}`);
if (!response.ok) throw new Error('Failed to fetch post');
return response.json();
}
이제 이 패턴으로 변환해주세요:
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
}
결과는? 타입 정의, 에러 핸들링, async/await까지 완벽한 코드.
Few-shot은 스타일 가이드를 학습시키는 것과 같다. 예시 2-3개면 AI는 패턴을 파악하고 일관된 출력을 낸다.
복잡한 문제를 AI한테 던지면 중간 단계를 건너뛰고 잘못된 답을 낸다. Chain-of-Thought는 "생각 과정을 보여줘"라고 요청하는 기법이다.
일반 프롬프트:이 알고리즘의 시간복잡도를 계산해줘
[코드]
답: "O(n^2)입니다." (틀렸음)
Chain-of-Thought 프롬프트:다음 알고리즘의 시간복잡도를 단계별로 분석해주세요.
1단계: 각 루프가 몇 번 실행되는지 파악
2단계: 중첩 루프의 관계 분석
3단계: 지배적인 항 찾기
4단계: Big-O 표기
[코드]
답: 단계별 분석과 함께 정확한 O(n log n) 도출.
복잡한 디버깅에도 쓴다.
이 버그의 원인을 다음 순서로 분석해주세요:
1. 에러 메시지 해석
2. 스택 트레이스에서 발생 지점 특정
3. 해당 코드의 입력값 추론
4. 예상 동작 vs 실제 동작 비교
5. 근본 원인 가설
6. 수정 방법 제안
[에러 로그와 코드]
AI한테 사고 과정을 강제하면, 정확도가 올라간다. 마치 러버덕 디버깅처럼, AI가 스스로 논리를 검증한다.
API를 쓸 때 system prompt를 설정하면, 모든 대화에서 일관된 동작을 유지한다. ChatGPT나 Claude UI에선 못 하지만, API나 Custom GPT에선 가능하다.
const systemPrompt = `
당신은 TypeScript/React 전문 코드 리뷰어입니다.
규칙:
- 모든 코드는 TypeScript strict mode 기준
- React는 함수형 컴포넌트와 hooks만 사용
- Tailwind CSS 권장, inline style 금지
- 성능 이슈 발견 시 반드시 지적
- 접근성 문제 지적 (ARIA, 키보드 네비게이션)
출력 형식:
1. 긍정적 피드백 (잘된 점)
2. 개선 필요 사항 (우선순위 순)
3. 수정된 코드 예시
`;
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: "이 컴포넌트 리뷰해줘\n[코드]" }
]
});
System prompt는 AI의 "성격"을 정의한다. 한 번 설정하면 매번 반복 안 해도 된다. 팀에서 공유하는 코드 스타일 가이드처럼 쓸 수 있다.
몇 가지 패턴은 거의 매일 쓴다. 템플릿화해서 저장해두면 편하다.
AI에게 특정 전문가 역할을 준다.
당신은 성능 최적화 전문가입니다.
다음 React 컴포넌트에서 불필요한 리렌더링을 찾아주세요.
정확한 출력 형식을 명시한다. JSON, Markdown, 코드만 등.
다음 형식의 JSON으로만 출력해주세요. 설명 없이 JSON만.
{
"issue": "문제 설명",
"solution": "해결 방법",
"code": "수정된 코드"
}
복잡한 작업을 단계로 나눈다.
다음 단계로 리팩토링해주세요:
1단계: 중복 코드 제거
2단계: 함수 분리 (단일 책임)
3단계: 타입 안정성 강화
4단계: 테스트 작성
각 단계마다 변경 사항과 이유를 설명해주세요.
먼저 하지 말아야 할 것을 명시한다.
제약조건 (절대 위반 금지):
- 기존 API 엔드포인트 변경 금지
- 데이터베이스 스키마 변경 금지
- 외부 라이브러리 추가 금지
이 제약 내에서 성능을 개선해주세요.
일반 사용자와 개발자의 프롬프트는 다르다. 코드 생성에 특화된 팁들.
// 나쁨
React 컴포넌트 만들어줘
// 좋음
Next.js 13 App Router, TypeScript, Server Component로 만들어줘
같은 "React"도 버전, 프레임워크, 렌더링 방식에 따라 완전히 다르다.
현재 프로젝트 구조:
src/
app/
(auth)/
login/
components/
ui/ (shadcn)
lib/
supabase.ts
이 구조에 맞춰 회원가입 페이지 추가해줘
기존 패턴을 보여주면 일관된 코드가 나온다.
다음 에러 디버깅해줘:
에러 메시지:
[전체 에러 로그, 스택 트레이스 포함]
관련 코드:
[에러 발생 지점 코드]
환경:
- Node.js 18.17
- Next.js 14.0.3
- 로컬 환경에서만 발생, production은 정상
에러 로그는 절대 요약하지 마라. 전부 붙여넣어야 정확한 진단이 나온다.
우리 팀 코드 스타일:
// API 호출
export async function getUser(id: string) {
const supabase = createClient();
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', id)
.single();
if (error) throw new Error(error.message);
return data;
}
이 패턴으로 getPosts 함수 만들어줘
팀 컨벤션을 예시로 주면, 코드 리뷰 지적사항이 줄어든다.
// 나쁨
"성능 최적화해줘"
// 좋음
"React 컴포넌트에서 useMemo, useCallback을 사용해 불필요한 리렌더링 제거해줘"
"최적화"는 백만 가지 의미다. 구체적으로 말해야 한다.
5000줄 코드를 붙여넣고 "버그 찾아줘"라고 하면, AI는 길을 잃는다. 핵심만 추려야 한다.
"TypeScript 쓰되, any 타입 써도 돼"
"성능 중요하지만, 가독성 우선"
우선순위를 명확히 해야 한다. "성능 critical, 가독성은 secondary"처럼.
// 나쁨
"이 에러 뭐야?"
[에러 메시지만]
// 좋음
"Next.js 14 App Router에서 Server Action 실행 시 발생하는 에러입니다.
환경: Node 18, TypeScript 5.3
시도한 것: 'use server' 추가, async 함수 확인
[에러 메시지 + 코드]"
AI는 독심술사가 아니다. 배경 정보가 필요하다.
첫 프롬프트가 완벽할 순 없다. 코드 디버깅처럼, 프롬프트도 반복 개선한다.
React 테이블 컴포넌트 만들어줘
결과: 기본적인 table 태그.
TypeScript, Tailwind CSS 사용
정렬 기능 포함
페이지네이션 필요
결과: 더 나아졌지만 디자인이 이상함.
shadcn/ui Table 컴포넌트 스타일로
다음과 같은 구조:
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
</TableRow>
</TableHeader>
<TableBody>
...
</TableBody>
</Table>
결과: 거의 완벽.
- 정렬 화살표 아이콘 추가
- 빈 상태 처리
- Loading skeleton
결과: Production ready.
이 과정은 TDD와 비슷하다. 작게 시작해서, 점진적으로 요구사항을 추가하고, 결과를 확인하면서 수정한다. 한 번에 완벽한 프롬프트를 쓰려고 하지 마라. 빠르게 반복하는 게 더 효율적이다.
프롬프트 엔지니어링을 배우면서 깨달은 건, 이게 새로운 스킬이 아니라는 것이다. 개발자가 이미 하고 있는 일의 연장선이다.
함수를 설계할 때: 입력, 출력, 제약조건을 명확히 한다. API를 문서화할 때: 사용 예시, 에러 케이스를 명시한다. PR을 작성할 때: 컨텍스트, 변경 사항, 테스트 방법을 적는다.
프롬프트도 똑같다. AI는 팀원이라고 생각하면 된다. 애매하게 말하면 애매한 결과가 나온다. 구체적으로, 구조적으로 전달하면 원하는 결과를 얻는다.
"로그인 만들어줘"는 "뭐 좀 해줘"만큼 쓸모없는 요청이다. 역할, 컨텍스트, 제약, 예시를 주면, AI는 내가 원하는 코드를 정확히 만든다.
프롬프트는 결국 인터페이스다. 잘 설계된 API처럼, 잘 설계된 프롬프트는 예측 가능하고 재사용 가능하다. 그리고 이건 앞으로 더 중요해질 스킬이다. AI와 일하는 시대에, 프롬프트는 코드만큼 중요한 자산이 될 거다.