1. 세션 요약 - 빈칸 채우기
(기본 원칙) 데이터를 사용하는 모든 곳에서 _______하지 않고, 함수 _______에서 원하는 형태로 _______하여 내부에서는 _______을 보장한다. 이를 통해 동시에 고려해야 하는 분기의 가짓수를 줄여 코드 가독성을 개선하고, 인지 부하를 낮출 수 있다.
('Parse, Don’t Validate' 패턴 전문가 5단계)
1.
사용하려는 데이터의 값이나 타입이 명확하지 않다면, 해당 데이터의 _______을 찾아 올라간다.
2.
해당 데이터가 불명확한 값을 갖는 이유를 확인하고, 예외에 대한 _______와 _______을 따져본다.
3.
예외와 경계 값은 throw 등을 활용하여 경계 바깥에서 처리하고, 경계 내부에서는 _______만 남도록 처리한다.
4.
이후 예외 케이스를 발라낸 데이터가 "_______한다"는 전제 하에 성공 케이스에 대해서만 코드를 작성한다.
5.
이 과정에서 코드를 전반적으로 살펴보면서 남아있는 불필요한 분기와 기본 값이 없는지 확인한다.
2. 세션 목표
개발하다 보면 특정 데이터의 유효성을 검증하기 위해 너무 많은 조건문을 작성하게 되고, 코드가 금방 복잡해져 알아보기 어렵게 됩니다.
•
같은 데이터를 여러 곳에서 반복 검증하고 있음
•
?.와 if (value) 체크, 기본 값 처리, 타입 단언 등이 코드 전체에 퍼져있음
•
예외 값을 제대로 처리하지 못해 “Cannot read property ‘xxx’ of undefined” 같은 런타임 에러를 종종 마주침
이번 세션에서 다루는 내용을 잘 이해하면,
•
하나의 데이터가 여러 가지 분기를 가질 때 원하는 타입의 데이터가 무조건 존재한다는 전제하에 쓸 수 있게 됩니다.
•
데이터의 상태를 개별적으로 검증하는 불필요한 조건문을 없애고 코드의 핵심적인 부분만 간결하게 유지할 수 있게 됩니다. 이를 통해 인지부하를 낮추고 코드의 유지보수성을 높일 수 있습니다.
3. 인접 개념
•
Early Return: 함수 시작 부분에서 예외 케이스를 빠르게 처리하고 주요 로직만 남기는 패턴
•
Type Narrowing: TypeScript에서 조건문을 사용해 더 구체적인 타입으로 좁히는 기법
•
Railway Oriented Programming(ROP): 성공과 실패를 별도 트랙으로 처리하여 예외 흐름을 깔끔하게 관리하는 방식
•
Fail Fast: 오류가 발생하는 즉시 실패하여 문제를 빠르게 드러내는 원칙
4. 멘탈 모델 & 시각화 자료
// ❌ Before: 계속되는 의심
function processOrder(orderId?: string) {
// 🤔 "orderId 있나?"
if (!orderId) return;
// 🤔 "숫자로 변환되나?"
const id = Number(orderId);
if (isNaN(id)) return;
// 🤔 "order 있나?"
const order = getOrder(id);
if (!order) return;
// 🤔 "items 있나? 배열인가?"
const items = order?.items || [];
// 🤔 "price 있나? 숫자인가?"
const total = items.reduce((sum, item) =>
sum + (Number(item?.price) || 0), 0
);
}
// ✅ After: 값이 무조건 유효하다는 전제하에 코딩하기
function processOrder(orderId: number) { // ✅ 무조건 number
const order = getOrder(orderId); // ✅ 무조건 Order
const total = order.items.reduce( // ✅ 무조건 배열
(sum, item) => sum + item.price, 0 // ✅ 무조건 number
);
}
TypeScript
복사
5. Parse, Don’t Validate 패턴이란?
if (data && data.user && data.user.profile) // ...
const name = response.data?.name; // ...
TypeScript
복사
이런 코드를 작성할 때, 우리는 '안전한' 코드를 짜고 있다고 생각합니다.
하지만 실제로는 코드의 모든 경계에서 "이 값이 정말 있을까?"라는 불안감과 싸우고 있는 것입니다.
우리는 코드 전체에 걸쳐 수많은 if문과 옵셔널 체이닝(?.)이라는 '방어벽'을 쌓아 올리며, 끊임없이 여러 가능성을 동시에 생각해야 하는 인지적 대가를 치릅니다.
이것은 '한번에 하나씩' 원칙의 정반대입니다. 코드의 모든 부분이 수많은 불확실한 시나리오를 걱정하고 있기 때문입니다. 'Parse, Don't Validate' 패턴을 따르면 이 소모적인 방어전을 끝낼 수 있습니다.
이 패턴의 핵심 아이디어는 간단합니다. 일종의 ‘얼리 리턴(Early Return)’ 이기도 합니다.
데이터를 사용하는 모든 곳에서 의심하며 검증(Validate)하지 말고, 데이터가 우리 시스템에 들어오는 가장 첫 관문에서 우리가 원하는 형태로 파싱(Parse)하라. 그 이후의 모든 곳에서는 타입과 구조를 100% 보장할 수 있다.
입국 심사가 없는 나라의 프로세스를 생각해보겠습니다. 거리에 돌아다니는 사람 중 누가 입국이 허락된 사람인지 알 수 없습니다. 허가된 사람인지 확인하려면 "신분증 좀 보여주시겠어요?"라며 매번 마주칠 때마다 물어봐야(validate) 합니다.
반면 입국 심사가 있는 국가에서는 한번 입국 심사를 받으면, 이후 모든 곳에서는 더 이상 여권을 검사할 필요가 없습니다. 입국 심사대에서 허가된 사람만 통과(parse)시키고, 아닌 사람은 별도 조치합니다.
이 원칙을 통해 우리는 불확실성을 시스템의 가장 바깥 경계 너머에 격리시킵니다. 통과한 데이터는 모두 믿을 수 있는 상태로 사용하게 됩니다.
경계 값 배제하기
URL 파라미터는 항상 string | undefined입니다. 매번 검증하지 말고, 경계에서 한 번만 파싱하세요.
// ❌ Bad: 반복적인 검증
function OrderDetail() {
const { orderId } = useParams() as { orderId: number };
if (!orderId) return <ErrorPage />;
const id = Number(orderId);
if (isNaN(id)) return <ErrorPage />;
// 여전히 orderId를 의심하며 사용
const order = useOrder(id); // id가 정말 number일까?
}
// ✅ Good: 경계에서 파싱
// hooks/useOrderId.ts
export function useOrderId(): number {
const { orderId } = useParams();
const parsed = Number(orderId);
if (!orderId || isNaN(parsed)) {
throw new Error("Invalid order ID in URL"); // 에러 케이스에 대한 처리는 필요
}
return parsed;
}
// components/OrderDetail.tsx
function OrderDetail() {
const orderId = useOrderId(); // number 타입 보장
const order = useOrder(orderId); // 확신을 가지고 사용
return <OrderView order={order} />;
}
TypeScript
복사
데이터 페칭 시 loading, error 케이스 배제하기
API 응답은 완료되었을 수도 있지만, 아직 로딩 중일 수도 있습니다. 로딩 상태는 배제하고 호출에 성공했다는 케이스에 대해서만 집중적으로 다루세요.
이외의 상태는 컴포넌트 바깥으로 밀어낸 다음 별도로 처리하세요.
Validate 방식 (방어적 로딩 처리)
// ❌ 모든 컴포넌트가 로딩 상태를 "validate" + 데이터 사용처마다 분기 처리
function UserProfile() {
const { data, isLoading } = useQuery(userOptions());
if (isLoading) return <Skeleton />; // 매번 체크
const handleClick = () => {
if (isLoading) { // 매번 체크
console.log(data)
}
}
return <div>{data.name}</div>;
}
TypeScript
복사
Parse 방식 (경계에서 로딩 처리)
// ✅ 경계에서 한 번만 "parse" (로딩 처리)
function UserPage() {
return (
<Suspense fallback={<PageSkeleton />}>
{/* 경계 */}
<UserProfile />
</Suspense>
);
}
// 내부는 데이터가 항상 있다고 신뢰
function UserProfile() {
const { data } = useSuspenseQuery(userOptions());
return <div>{data.name}</div>; // 바로 사용!
}
TypeScript
복사
Form 제출 전 기대하지 않는 형태 이외 케이스 배제하기
Form 데이터는 다양한 형태로 들어올 수 있습니다. 제출 전에 파싱하여 예상치 못한 케이스를 배제하세요.
// ❌ Bad: 불확실한 form 데이터
function handleSubmit(formData: FormData) {
const email = formData.get("email"); // string | File | null
const age = formData.get("age");
if (email && typeof email === "string") {
if (age && !isNaN(Number(age))) {
// 중첩된 조건문...
}
}
}
// ✅ Good: 파싱 함수를 조합하여 명확하고 안전한 데이터 보장
function handleSubmit(formData: FormData) {
try {
// 각 파싱 함수를 순차적으로 호출합니다.
// 여기서 에러가 발생하면 즉시 catch 블록으로 이동합니다.
const email = getEmail(formData);
const age = getAge(formData);
// `email`은 string, `age`는 number, `hasAgreed`는 boolean 타입임이 100% 보장됩니다.
console.log("✅ API로 전송할 데이터:", { email, age });
} catch (error) {
if (error instanceof Error) {
console.error("❌ 유효성 검사 실패:", error.message);
} else {
console.error("알 수 없는 에러가 발생했습니다.");
}
}
}
TypeScript
복사
기타 예시
6. 전문가들의 Parse, Don’t Validate 패턴
6-1. 미니 과제 - Before
6-2. 전문가 영상
전문가들은 ’Parse, Don’t Validate’ 패턴을 적용할 때 아래와 같은 단계를 거칩니다:
1.
사용하려는 데이터의 값이나 타입이 명확하지 않다면, 해당 데이터의 시작 지점을 찾아 올라간다.
•
ex. 서버에서 받아온 data.products가 Products[] | undefined일 수 있는 경우
•
ex. URL 쿼리 파라미터에서 받아온 userId가 string | undefined 타입으로 들어오지만 실제로는 number 타입이 필요한 경우
•
ex. onSubmit 핸들러에 전달된 이벤트 객체에서 FormData를 꺼내어 값의 형태와 타입을 보장할 수 없는 경우
2.
형태가 불명확한 이유를 확인하고, 해당 데이터가 가질 수 있는 예외에 대한 경우의 수와 경계 값을 따져본다.
•
ex. useQuery로 받아온 비동기 데이터이므로 1) 로딩 2) 성공 상태가 섞여 있으므로 타입에 undefined가 섞여 있음
•
ex. userId가 0, -1 등 정상 범위 바깥에 있거나, 사용자 입력에 따라 숫자가 아닌 일반 string이 들어올 수 있음
•
ex. new FormData(event.target) 방식으로 꺼낼 경우 자동으로 내부의 타입을 추론하지 못하기 때문에 검증이 필요함
3.
예외와 경계 값은 throw 등을 활용하여 경계 바깥에서 처리하고, 경계 내부에서는 성공 케이스만 남도록 처리한다.
•
ex. useSuspenseQuery를 사용하여 로딩 상태를 상위 컴포넌트에서 처리하고, 데이터 페칭 컴포넌트에서는 데이터가 항상 존재한다고 가정
•
ex. throw 문이나 zod의 parse(), isNaN(), 쿼리스트링을 다루는 nuqs 등의 라이브러리를 활용하여 쿼리 파라미터를 파싱하고, 유효하지 않은 데이터는 별도의 에러 케이스로 처리하여 컴포넌트에는 항상 유효한 데이터만 다루도록 함
•
ex. 입력 폼에서 받아온 데이터를 파싱하고 예외는 던지는 함수를 작성한다. 기대한 형태의 데이터일 경우로만 타입을 좁혀 API 호출 로직을 작성하고, 나머지는 예외로 처리함
4.
이후 성공 케이스만 발라낸 데이터가 “무조건 존재한다”는 전제 하에 편하게 사용한다.
5.
코드를 전반적으로 살펴보면서 남아있는 불필요한 분기와 기본 값, 타입 단언 등이 없는지 확인한다.
6-3. 미니 과제 - After
7. Wrap-up
(기본 원칙) 데이터를 사용하는 모든 곳에서 검증하지 않고, 함수 경계에서 원하는 형태로 파싱하여 내부에서는 데이터 유효성을 보장한다. 이를 통해 동시에 고려해야 하는 분기의 가짓수를 줄여 코드 가독성을 개선하고, 인지 부하를 낮출 수 있다.
('Parse, Don’t Validate' 패턴 전문가 5단계)
1.
사용하려는 데이터의 값이나 타입이 명확하지 않다면, 해당 데이터의 시작 지점을 찾아 올라간다.
2.
해당 데이터가 불명확한 값을 갖는 이유를 확인하고, 예외에 대한 경우의 수와 경계 값을 따져본다.
3.
예외와 경계 값은 throw 등을 활용하여 경계 바깥에서 처리하고, 경계 내부에서는 성공 케이스만 남도록 처리한다.
4.
이후 예외 케이스를 발라낸 데이터가 "무조건 존재한다"는 전제 하에 성공 케이스에 대해서만 코드를 작성한다.
5.
이 과정에서 코드를 전반적으로 살펴보면서 남아있는 불필요한 분기와 기본 값이 없는지 확인한다.
