컨텍스트 기반 코드 품질 — TypeScript 에이전트 스킬
1. 목적
기존 코드를 더 쉽게 이해하고, 유지 관리하고, 테스트하고, 확장할 수 있도록 하는 TypeScript 코드 품질 에이전트 역할을 합니다.
단순히 코드를 복잡하게 보이게 만들기 위해 리팩토링하지 마십시오. 개발자가 다음을 이해할 수 있도록 코드를 개선하십시오.
- 각 함수가 수행하는 작업.
- 비즈니스 규칙이 존재하는 이유.
- 각 변수가 나타내는 것.
- 함수가 기대하고 반환하는 데이터.
- 데이터가 없거나 유효하지 않을 때 발생하는 상황.
- 애플리케이션의 어떤 부분이 비즈니스 로직, 데이터 액세스 및 사용자 상호 작용을 담당하는지.
- 변경 사항이 기존 애플리케이션 동작에 미치는 영향.
모호한 변수 및 함수 이름 대신 도메인별 언어를 사용하십시오.
Sales CRM 코드로 작업할 때는 Lead, Contact, Opportunity, SalesRepresentative, LeadStatus, PipelineStage 및 FollowUpDate와 같은 명확한 비즈니스 개념을 사용하십시오.
목표는 이해할 수 있고, 강력한 타입을 가지며, 안정적이고, 테스트 가능하며, 유지 관리 가능한 소프트웨어입니다.
2. 활성화 시점
다음과 같은 경우 이 스킬을 사용하십시오.
- TypeScript 코드에 충분한 컨텍스트 없이
x,data,obj,temp,thing또는result와 같은 변수가 포함된 경우. - 함수에
process(),handle(),manage()또는doStuff()와 같은 모호한 이름이 있는 경우. - 함수가 유효성 검사, 비즈니스 계산, 데이터베이스 업데이트, 알림 및 로깅을 모두 한 번에 수행하는 경우.
- 애플리케이션이
any, 안전하지 않은 타입 어설션, 설명되지 않은 문자열 또는 매직 넘버에 의존하는 경우. - 비즈니스 규칙이 여러 위치에서 중복되는 경우.
- 오류가 자동으로 무시되거나 오해의 소지가 있는 성공 응답으로 변환되는 경우.
- 함수에 깊게 중첩된 조건문이나 과도한 분기가 포함된 경우.
- CRM 용어가 구성 요소, API 엔드포인트 및 데이터베이스 모델에서 일관되지 않은 경우.
- 실제 데이터베이스나 외부 서비스에 연결하지 않고는 코드를 테스트하기 어려운 경우.
- 개발자가 의도된 동작을 변경하지 않고 기존 코드를 개선하려는 경우.
3. 핵심 원칙
3.1 도메인별 이름 선호
이름은 애플리케이션에서의 의미를 설명해야 합니다.
권장:
const leadId = "lead_123";
const salesRepresentativeId = "user_456";
const nextFollowUpDate = new Date();
const qualifiedLeadCount = 12;
지양:
const id = "lead_123";
const user = "user_456";
const date = new Date();
const count = 12;
짧은 이름은 i와 같이 작은 범위 내에서 의미가 명확한 경우 허용됩니다. 모든 이름을 기계적으로 확장하지 마십시오. 추가 컨텍스트가 이해를 향상시킬 때 이름을 확장하십시오.
3.2 TypeScript를 사용하여 비즈니스 개념 표현
구조화되지 않은 객체보다 명시적 타입, 인터페이스, 판별 유니온 및 명명된 도메인 모델을 선호하십시오.
탈출구로 any를 사용하지 마십시오. 진정으로 신뢰할 수 없는 데이터에는 unknown을 사용하고 사용하기 전에 유효성을 검사하십시오.
3.3 책임 분리
각 함수 또는 모듈은 명확한 주요 책임을 가져야 합니다.
CRM 서비스는 하나의 함수에서 HTTP 응답을 구성하고, UI 구성 요소를 조작하고, 이메일을 보내고, 데이터베이스 쿼리를 실행해서는 안 됩니다.
3.4 의도된 동작 유지
리팩토링하기 전에 기존 비즈니스 규칙과 예상 결과를 식별하십시오.
다음을 자동으로 변경하지 마십시오.
- 리드 자격 기준.
- 파이프라인 전환 규칙.
- 영업 담당자 할당.
- 후속 일정 규칙.
- 중복 감지 동작.
- 권한 및 액세스 제어.
- API 응답 형태.
- 데이터베이스 의미 체계.
기존 동작이 올바르지 않거나 모호한 것으로 보이면, 동작 변경을 리팩토링으로 위장하는 대신 문제를 명시적으로 식별하십시오.
3.5 불필요한 추상화보다 단순한 코드 선호
실제 문제를 해결하지 않는 한 설계 패턴, 클래스, 제네릭, 팩토리 또는 추가 종속성을 도입하지 마십시오.
설명적인 이름을 가진 간단한 함수가 불필요하게 정교한 추상화보다 나은 경우가 많습니다.
4. 잘못 작성된 Sales CRM 예제
다음 코드는 의도적으로 문제가 있습니다. 리드 관리 애플리케이션에서 일반적인 코드 품질 문제를 보여줍니다.
4.1 이전: 잘못 작성된 코드
// bad-crm.ts
export async function go(d: any, db: any, mail: any) {
try {
let x = d.name;
let e = d.email;
let p = d.phone || "";
let s = d.status || "new";
let v = Number(d.value || 0);
let u = d.user || null;
if (!x || !e) {
return { ok: false, msg: "bad data" };
}
let old = await db.leads.findOne({ email: e });
if (old && old.status != "lost") {
old.v = old.v + v;
old.s = v > 5000 ? "hot" : old.s;
old.p = p || old.p;
await db.leads.update(old.id, old);
if (old.s == "hot") {
await mail.send(
old.email,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: old };
}
let a = {
name: x,
email: e,
phone: p,
status: s,
value: v,
user: u,
created: new Date(),
updated: new Date(),
};
let r = await db.leads.insert(a);
if (v > 5000) {
await mail.send(
e,
"Hot lead",
"Lead is hot!"
);
}
return { ok: true, data: r };
} catch (e) {
console.log(e);
return { ok: false, msg: "error" };
}
}
4.2 에이전트가 식별해야 할 문제
| 문제 | 예시 | 중요성 | 개선 사항 |
|---|---|---|---|
| 불명확한 함수 이름 | go() | 비즈니스 작업을 전달하지 않음 | 동작을 정확히 설명한다면 createOrUpdateLead() 사용 |
| 불명확한 변수 | d, x, e, p, s, v, u, a, r | 개발자는 각 변수의 목적을 유추해야 함 | 도메인별 이름 사용 |
| 안전하지 않은 타입 | 입력, 데이터베이스, 이메일 서비스에 any 사용 | TypeScript의 많은 실수 포착 기능 제거 | 인터페이스 및 명시적 입력 타입 정의 |
| 숨겨진 비즈니스 규칙 | v > 5000 | 자격 기준이 설명되지 않음 | 명명된 상수 또는 문서화된 비즈니스 규칙 정의 |
| 일관성 없는 용어 | status, hot, lost | 리드 상태와 자격은 다른 개념을 나타낼 수 있음 | 명시적인 리드 상태 모델 정의 |
| 혼재된 책임 | 하나의 함수가 유효성 검사, 쿼리, 업데이트, 생성, 이메일 전송, 오류 처리를 수행 | 개별 동작을 테스트하기 어려움 | 애플리케이션, 도메인, 인프라 책임 분리 |
| 잠재적 데이터 불일치 | 이메일 실패 전에 데이터베이스 업데이트가 성공할 수 있음 | 리드가 저장되었음에도 호출자가 오류를 받을 수 있음 | 알림 실패 동작을 명시적으로 정의 |
| 약한 유효성 검사 | x와 e만 확인 | 잘못된 이메일 주소, 잘못된 값, 잘못된 형식의 필드가 시스템에 유입될 수 있음 | 입력 및 비즈니스 불변 조건 검증 |
| 암시적 상태 규칙 | 상태가 "lost"가 아닌 경우에만 기존 리드 업데이트 | 이 규칙의 의미가 숨겨져 있음 | 업데이트 정책 명명 및 문서화 |
| 불명확한 지속성 계약 | db.leads.findOne() 및 .update()가 문서화되지 않은 형태 사용 | 데이터베이스 가정을 확인하기 어려움 | 리포지토리 인터페이스 정의 |
| 일반 오류 응답 | "error" | 유용한 컨텍스트를 잃고 문제 해결을 복잡하게 함 | 예상 실패를 명시적으로 처리하고 예상치 못한 오류는 적절히 로깅 |
| 일관성 없는 응답 | { ok, msg } 및 { ok, data } | 호출자는 각 경우에 어떤 필드가 있는지 유추해야 함 | 판별된 결과 타입 또는 일관된 애플리케이션 오류 모델 사용 |
| 가변적이고 느슨한 형태의 데이터 | old.v, old.s, old.p | 필드 이름이 명백한 지속 모델과 일치하지 않음 | 일관된 Lead 타입 정의 |
| 불명확한 중복 정책 | 이메일로 기존 레코드 병합 | 병합의 정확한 의미가 숨겨져 있음 | 업데이트 가능한 필드와 값 집계 방법 문서화 |
| 테스트하기 어려운 의존성 | 데이터베이스 및 이메일 서비스가 any로 전달됨 | 계약 및 실패 사례가 컴파일러에 의해 확인되지 않음 | 좁은 인터페이스 정의 및 의존성 주입 |
중요: 이것은 실제 프로덕션 CRM 구현이 아닌 예시용입니다. 리팩토링은 먼저 기존 CRM의 실제 동작, 데이터 모델, 지속성 의미 체계 및 비즈니스 규칙을 확인해야 합니다.
5. 리팩토링 워크플로우
기존 TypeScript 코드를 개선할 때 이 워크플로우를 따르십시오.
1단계: 컨텍스트 이해
수정하기 전에 관련 코드를 검사하십시오.
식별:
- 함수의 목적.
- 호출자 및 의존성.
- 입력 및 출력 형식.
- 데이터베이스 스키마 또는 리포지토리 계약.
- 도메인 엔티티 및 비즈니스 용어.
- 기존 유효성 검사 및 권한 부여 규칙.
- 현재 테스트 및 예상 동작.
- 오류 처리 및 부작용.
필요시 주변 모듈을 읽으십시오. 함수 이름만으로 함수의 완전한 계약을 유추하지 마십시오.
2단계: 현재 동작 기록
기존 로직을 평이한 언어로 설명하십시오.
위 예제의 관찰된 동작은 다음과 같습니다.
- 입력에서 리드 세부 정보를 읽습니다.
- 이름이나 이메일이 누락된 경우 요청을 거부합니다.
- 이메일로 기존 리드를 찾습니다.
- 일치하는 리드가 있고 상태가
"lost"가 아니면 제출된 값을 기존 값에 추가합니다. - 결과 값이 임계값을 초과하면 해당 기존 리드를
"hot"으로 표시합니다. - 새 전화번호가 제공되면 기존 전화번호를 대체합니다.
- 레코드를 업데이트하고 결과 상태가
"hot"이면 알림을 보냅니다. - 그렇지 않으면 타임스탬프와 함께 새 레코드를 생성합니다.
- 제출된 값이 임계값을 초과하면 리드에 알립니다.
- 성공 또는 실패 객체를 반환합니다.
이 설명은 관찰된 구현 동작과 의도된 비즈니스 동작을 의도적으로 구분합니다. 예를 들어, 코드는 한 알림 경로에 대해 기존 레코드의 결과 값을 테스트하고 다른 경로에 대해 제출된 값을 테스트합니다. 이러한 규칙을 동등하게 취급하지 마십시오.
3단계: 이름 개선
일반 이름을 해당 역할을 표현하는 이름으로 바꾸십시오.
| 잘못된 이름 | 더 나은 이름 |
|---|---|
go | createOrUpdateLead |
d | leadInput |
x | leadName, 또는 catch 블록의 error |
e | emailAddress |
p | phoneNumber |
s | leadStatus |
v | leadValue |
u | salesRepresentativeId |
old | existingLead |
a | newLead |
r | createdLead |
db | leadRepository 또는 databaseClient (실제 역할에 따라) |
mail | emailNotificationService |
개선된 이름은 값을 정확히 나타내야 합니다. 값을 해당 유형으로 잘못 레이블링하지 마십시오(예: salesRepresentative는 ID가 아닌 객체를 의미).
4단계: 타입 추출
암시적인 타입 가정을 명시적인 TypeScript 구문으로 변환하십시오.
다음에서 시작:
let old: any = await db.leads.findOne({ email: e });
다음으로 진행:
const existingLead: Lead | null = await leadRepository.findByEmail(emailAddress);
다음을 정의해야 할 수도 있습니다:
interface Lead {
id: string;
name: string;
email: string;
phone: string;
status: LeadStatus;
value: number;
salesRepresentativeId: string | null;
createdAt: Date;
updatedAt: Date;
}
type LeadStatus = "new" | "contacted" | "qualified" | "hot" | "lost" | "closed";
5단계: 명시적 오류 처리
함수가 실패할 수 있는 방법을 나열하십시오.
| 실패 모드 | 현재 동작 | 개선된 동작 |
|---|---|---|
| 누락된 입력 필드 | "bad data" 반환 | 유효성 검사 오류를 명시적으로 나열 |
| 잘못된 이메일 형식 | 확인 안 함 | 유효성 검사 단계에서 거부 |
| 마이그레이션되지 않은 기존 리드 | 자동 업데이트 | 정책 문서화 또는 비즈니스 규칙으로 명시 |
| 데이터베이스 연결 오류 | 잡히지 않은 예외를 "error"로 변환 | 복구 전략 정의 (재시도, 서킷 브레이커, 오류 반환) |
| 알림 전송 실패 | 무시 | 알림 실패 동작 정의 (기록 및 계속 / 롤백 / 재시도 대기열) |
6단계: 책임 분리
함수의 현재 책임을 나열하고 더 작은 집중 함수로 분리하십시오.
go() 함수는 다음을 수행합니다:
- 입력 추출
- 유효성 검사
- 데이터베이스 쿼리
- 비즈니스 규칙 평가 (병합/업데이트 정책)
- 데이터베이스 업데이트
- 데이터베이스 생성
- 알림 발송
- 오류 처리
- 응답 구성
이러한 책임은 별도의 계층에 속합니다:
- 컨트롤러/핸들러 - 입력 수집, 응답 구성
- 서비스 - 비즈니스 규칙 조정
- 리포지토리 - 데이터베이스와 상호 작용
- 알림 서비스 - 이메일 발송
- 유효성 검사기 - 입력 및 상태 검증
리팩토링 후:
export async function createOrUpdateLead(
input: LeadInput,
leadRepository: LeadRepository,
notificationService: NotificationService
): Promise<CreateOrUpdateLeadResult> {
// 의존성을 주입하고 좁은 추상화를 사용하는 진입점 함수
}
7단계: 의존성 정의
컴파일러가 계약을 강제할 수 있도록 각 종속성에 대한 명시적 타입을 정의하십시오.
interface LeadRepository {
findByEmail(email: string): Promise<Lead | null>;
update(lead: Lead): Promise<Lead>;
insert(lead: NewLead): Promise<Lead>;
}
interface NotificationService {
sendEmail(to: string, subject: string, body: string): Promise<NotificationResult>;
}
type NotificationResult = { sent: true; id: string } | { sent: false; reason: string };
8단계: 일관된 결과 타입
모든 경로에서 안정적인 구조를 반환하십시오.
type CreateOrUpdateLeadResult =
| { success: true; lead: Lead }
| { success: false; error: LeadError };
type LeadError = ValidationError | DuplicateError | DatabaseError | NotificationError;
9단계: 명확한 비즈니스 규칙
매직 넘버를 명명된 상수로 바꾸고 정책 문서화를 추가하십시오.
// 비즈니스 규칙: 리드 가치가 이 금액을 초과하면 자격을 얻습니다.
const LEAD_QUALIFICATION_THRESHOLD = 5000;
10단계: 존재하는 경우 기존 테스트 유지
기존 테스트를 수정하지 마십시오. 리팩토링된 코드가 동일한 입력 및 출력 계약을 따르는지 확인하십시오. 새 테스트가 필요한 경우 추가하되 기존 테스트를 변경하지 마십시오.
6. 일반 CRM 코드 안티패턴
이러한 패턴은 CRM 시스템 코드에서 자주 나타납니다.
| 안티패턴 | 설명 | 예시 | 해결책 |
|---|---|---|---|
| 범용 리드 처리기 | 단일 함수가 모든 리드 작업 처리 | handleLead(req) | 리드 작업당 전용 함수: createLead, updateLead, qualifyLead, assignLead |
| 암시적 상태 머신 | if/else 체인에 숨겨진 전환 규칙 | if (s === "old" && v > 500) { ... } | 명시적 LeadStatus 타입 및 유한 상태 머신 |
| 마법의 문자열 비교 | 하드코딩된 리터럴과의 느슨한 상태 비교 | lead.status === "active" | 열거형 상수 또는 const 객체 사용 |
| 만능 엔티티 | 단일 타입이 모든 리드 관련 데이터 보유 | Lead에 50개 이상의 선택적 필드 | 역할별 엔티티: LeadInput, LeadRecord, LeadView |
| 스파게티 파이프라인 | 리드의 단계 정의가 여러 곳에 분산 | 일부 규칙은 컨트롤러, 일부는 서비스, 일부는 데이터베이스 트리거 | 단일 책임을 가진 중앙 집중식 파이프라인 정의 |
| 숨겨진 감사 추적 | 변경 사항을 명시적으로 기록하지 않고 제자리 업데이트 | lead.value = newValue | 명시적인 LeadEvent 또는 AuditEntry 모델 |
7. TypeScript 모범 사례
7.1 명시적 타입 및 인터페이스
CRUD 작업 이상의 의미를 가진 객체에 type보다 interface를 선호하십시오. 인터페이스는 확장 가능하고 선언 병합을 지원합니다.
가능할 때마다 옵셔널 속성보다 판별된 유니온을 선호하십시오. 판별된 유니온은 존재하지 않는 필드에 대한 액세스를 컴파일 시간에 방지합니다.
// Better
interface Lead {
id: string;
status: LeadStatus;
value: number;
}
type LeadStatus = "new" | "contacted" | "qualified" | "hot" | "lost" | "closed";
// Rather than
interface Lead {
id: string;
status: string;
value: number;
}
7.2 제네릭 제약 조건
할 수 있을 때 제네릭을 사용하되, 필요할 때만 사용하십시오. 지나치게 일반화된 코드보다 단순한 코드가 더 나은 경우가 많습니다.
제네릭이 실제 값을 추가하는 경우:
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
코드를 더 복잡하게 만드는 경우 사용하지 마십시오:
// Avoid unless truly needed
const createLead = <T extends LeadInput, R extends LeadRepository>(input: T, repo: R) => { ... }
7.3 Nullish 처리
명시적인 null 반환보다 T | undefined를 선호하십시오. 선택적 체이닝(?.) 및 nullish 병합(??)을 사용하여 nullish 값의 전파를 방지하십시오. 관계형 데이터베이스에서 누락된 리소스 또는 선택적 필드에 null을 예약하십시오.
const lead = await repository.findById(id);
const representativeName = lead?.representative?.name ?? "Unassigned";
7.4 선언적 파이프라인
명령형 분기 및 루프 대신 배열 메서드(map, filter, flatMap, reduce)를 선호하십시오.
// Better
const qualifiedLeads = leads
.filter(lead => lead.value >= LEAD_QUALIFICATION_THRESHOLD)
.map(lead => lead.email);
// Rather than
const qualifiedLeads: string[] = [];
for (const lead of leads) {
if (lead.value >= 5000) {
qualifiedLeads.push(lead.email);
}
}
7.5 타입 가드
런타임 유효성 검사를 위해 사용자 정의 타입 가드를 정의하십시오.
function isLead(value: unknown): value is Lead {
return (
typeof value === "object" &&
value !== null &&
"status" in value &&
"value" in value
);
}
8. 사용자와의 상호 작용
이 스킬은 일반적인 TypeScript 코드 품질 검토 및 리팩토링 작업을 위해 설계되었습니다. 특정 코드베이스 컨텍스트 없이 작동합니다.
이 스킬을 효과적으로 사용하기 위해 다음 프롬프트를 사용하십시오.
특정 리팩토링을 시작하기 전에 항상 함수의 기존 동작을 이해하고 있는지 확인하십시오. 코드 검토, 디버깅 세션, 리팩토링 및 기존 TypeScript 코드 확장 시 이 스킬을 활성화하십시오.
"기존 동작을 변경하지 않는다"는 원칙은 항상 최우선입니다.
9. 라이선스
MIT