Search
🚧

5장_사례와_함정

5장. 사례와 함정

4장에서 코드 추상화의 원리를 다뤘어요. 이번 장에서는 실무에서 자주 만나는 사례피해야 할 함정을 볼게요.

실무에서 자주 만나는 사례

프론트엔드 개발에서 추상화가 가장 필요한 곳들이에요.

사례 1: Form

폼은 추상화가 가장 필요한 곳 중 하나예요.
Before: What/How가 뒤섞임
function SignupForm() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); const [errors, setErrors] = useState({}); const validate = () => { const newErrors = {}; if (!name) newErrors.name = "이름을 입력하세요"; if (!email) newErrors.email = "이메일을 입력하세요"; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = () => { if (validate()) { /* 제출 로직 */ } }; }
TypeScript
복사
모든 폼마다 비슷한 코드가 반복돼요.
After: What만 드러남
const { values, errors, handleChange, handleSubmit } = useForm({ initialValues: { name: "", email: "" }, validate: (values) => ({ name: values.name ? undefined : "이름을 입력하세요", email: values.email ? undefined : "이메일을 입력하세요", }), onSubmit: (values) => { /* 제출 로직 */ }, });
TypeScript
복사
What: “폼 상태를 관리하고 검증한다” How: useForm 안에 숨어있어요.

사례 2: List

목록 렌더링도 반복되는 패턴이에요.
Before
function UserList() { const [users, setUsers] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [page, setPage] = useState(1); useEffect(() => { setLoading(true); fetchUsers(page) .then(setUsers) .catch(setError) .finally(() => setLoading(false)); }, [page]); if (loading) return <Spinner />; if (error) return <Error />; return users.map((user) => <UserCard user={user} />); }
TypeScript
복사
After
const { data: users, isLoading, error } = useQuery( ["users", page], () => fetchUsers(page) ); if (isLoading) return <Spinner />; if (error) return <Error />; return users.map((user) => <UserCard user={user} />);
TypeScript
복사
What: “비동기 데이터를 불러와서 보여준다” How: useQuery 안에 숨어있어요.

사례 3: State

상태 관리에서 What/How가 섞이면 복잡해져요.
Before
function Cart() { const [items, setItems] = useState([]); const addItem = (item) => { const existing = items.find((i) => i.id === item.id); if (existing) { setItems( items.map((i) => i.id === item.id ? { ...i, quantity: i.quantity + 1 } : i ) ); } else { setItems([...items, { ...item, quantity: 1 }]); } }; const removeItem = (id) => { setItems(items.filter((i) => i.id !== id)); }; const total = items.reduce((sum, i) => sum + i.price * i.quantity, 0); }
TypeScript
복사
After
const { items, addItem, removeItem, total } = useCart(); // How는 커스텀 훅 안에 function useCart() { const [items, setItems] = useState([]); const addItem = (item) => { /* 복잡한 로직 */ }; const removeItem = (id) => { /* 로직 */ }; const total = useMemo(() => /* 계산 */, [items]); return { items, addItem, removeItem, total }; }
TypeScript
복사
What: “장바구니를 관리한다”

사례 4: API 호출

API 호출 패턴도 추상화 대상이에요.
Before
const response = await fetch("/api/users", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!response.ok) throw new Error("Failed"); return response.json();
TypeScript
복사
After
const user = await api.post("/users", data); // How는 api 클라이언트 안에 const api = { async post(url, data) { const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!response.ok) throw new Error("Failed"); return response.json(); }, };
TypeScript
복사
What: “API를 호출한다”

피해야 할 함정

함정 1: 표면대로 옮기면 망한다

Before: > “필드가 개인정보 텍스트면 마스킹하세요.” > “필드가 개인정보 첨부파일이면 마스킹하세요.”
// 원본: 마스킹 로직이 필요한 상황 function renderField(field: Field) { // 개인정보면 마스킹해야 함 return <FieldView field={field} />; }
TypeScript
복사
Bad: 잘못된 추상화 시도
// 요구사항을 표면 그대로 옮김 if (hasAttachment && isPrivacyText) { mask(); } if (hasAttachment && isPrivacyAttachment) { mask(); } if (!hasAttachment && isPrivacyText) { mask(); } if (!hasAttachment && isPrivacyAttachment) { mask(); } // 조건문 4개. 변경할 때 모든 조합을 다시 찾아야 함
TypeScript
복사
Good: 잘된 추상화
if (isMaskingRequired(field)) { mask(); } function isMaskingRequired(field: Field): boolean { return field.isPrivacy; }
TypeScript
복사
본질: 마스킹 조건의 핵심은 “개인정보 여부” 하나예요. 요구사항이 바뀌면 한 곳만 고치면 돼요.

함정 2: 얽힘 (Entanglement)

Before: > “유저 프로필 페이지를 만들어주세요. 수정 기능도 있어야 해요.”
// 원본 코드: 데이터 fetch, UI 상태, 검증이 한 곳에 섞여있음 function UserProfile() { const [user, setUser] = useState(null); const [isEditing, setIsEditing] = useState(false); useEffect(() => { fetch(`/api/users/${id}`).then((r) => r.json()).then(setUser); }, [id]); const handleSave = async () => { const isValid = user.name.length > 0 && user.email.includes("@"); if (!isValid) return alert("입력을 확인하세요"); await fetch(`/api/users/${id}`, { method: "PUT", body: JSON.stringify(user) }); setIsEditing(false); }; }
TypeScript
복사
Bad: 잘못된 추상화 시도
// 얽힘을 그대로 둔 채 훅으로만 빼냄 function useUserProfile(id) { const [user, setUser] = useState(null); const [isEditing, setIsEditing] = useState(false); // 여전히 모든 관심사가 하나의 훅에 섞여있음 // 데이터 + UI상태 + 검증이 분리되지 않음 return { user, isEditing, handleSave }; }
TypeScript
복사
Good: 잘된 추상화
function UserProfile() { const { user, updateUser } = useUser(id); // 데이터 레이어 const { isEditing, startEdit, endEdit } = useEditMode(); // UI 상태 const { validate } = useUserValidation(); // 검증 로직 const handleSave = async () => { if (!validate(user)) return; await updateUser(user); endEdit(); }; }
TypeScript
복사
본질: 관심사(데이터, UI상태, 검증)는 각각 독립적인 What이에요. 하나로 묶으면 변경 시 전체가 흔들려요.

함정 3: 흩어짐 (Scattering)

Before: > “로그인 기능을 추가해주세요. 토큰 기반 인증이에요.”
// 원본: 토큰 관련 코드가 여러 파일에 흩어져 있음 // Header.tsx if (localStorage.getItem("token")) { showLogout(); } // api.ts headers: { Authorization: `Bearer ${localStorage.getItem("token")}` } // LoginPage.tsx localStorage.setItem("token", response.token); // ProfilePage.tsx if (!localStorage.getItem("token")) redirect("/login");
TypeScript
복사
Bad: 잘못된 추상화 시도
// 토큰 키를 상수로만 뺌 - 여전히 흩어져 있음 const TOKEN_KEY = "token"; // Header.tsx - 여전히 직접 접근 if (localStorage.getItem(TOKEN_KEY)) { ... } // api.ts - 여전히 직접 접근 headers: { Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}` }
TypeScript
복사
Good: 잘된 추상화
// auth.ts - 한 곳에 모음 const auth = { getToken: () => localStorage.getItem("token"), setToken: (token) => localStorage.setItem("token", token), isLoggedIn: () => !!auth.getToken(), logout: () => localStorage.removeItem("token"), }; // 쓰는 곳에서는 auth만 참조 if (auth.isLoggedIn()) { ... }
TypeScript
복사
본질: “토큰 기반 인증”은 하나의 What이에요. 하나의 What은 한 곳에서 관리되어야 해요.

함정 4: 이른 추상화

Before: > “회원가입 폼을 만들어주세요.”
// 원본: 첫 번째 폼을 만드는 상황 function SignupForm() { const [name, setName] = useState(""); const [email, setEmail] = useState(""); // ... 폼 로직 }
TypeScript
복사
Bad: 잘못된 추상화 시도
// 첫 번째 폼인데 바로 범용 추상화 const UniversalForm = ({ config }) => { // 100줄의 복잡한 설정 기반 렌더링 // 아직 패턴이 안 보이는데 미리 추상화 }; <UniversalForm config={signupFormConfig} />
TypeScript
복사
Good: 잘된 추상화
// 1차: SignupForm - 그냥 작성 // 2차: LoginForm - 비슷하네, 메모 // 3차: ProfileForm - 패턴 보인다! 이제 추상화 const useForm = (config) => { // 세 번 반복된 패턴만 추출 };
TypeScript
복사
본질: 추상화할 패턴이 아직 없어요. 패턴은 반복에서 드러나요. “세 번째 보이면” 추상화해요.

함정 5: 과도한 추상화

Before: > “유저 데이터를 가져오는 훅을 만들어주세요.”
// 원본: 단순한 데이터 fetch 필요 function useUser(id) { const [user, setUser] = useState(null); useEffect(() => { fetch(`/api/users/${id}`).then(r => r.json()).then(setUser); }, [id]); return user; }
TypeScript
복사
Bad: 잘못된 추상화 시도
// 모든 경우를 다 커버하려는 과도한 추상화 interface DataLayerConfig<T, P, R, E> { fetcher: Fetcher<T, P>; transformer: Transformer<T, R>; errorHandler: ErrorHandler<E>; cacheStrategy: CacheStrategy; retryPolicy: RetryPolicy; } const useData = <T, P, R, E>(config: DataLayerConfig<T, P, R, E>) => { ... }; // 타입 파라미터 4개, 설정 5개. 쓰는 사람이 더 힘들어요.
TypeScript
복사
Good: 잘된 추상화
const { data } = useQuery(["user"], fetchUser);
TypeScript
복사
본질: “데이터를 가져온다”가 What이에요. 추상화는 복잡함을 줄일 때만 해요. 더 복잡해지면 안 해요.

함정 6: 잘못된 경계

Before: > “회원가입하면 저장하고 환영 이메일 보내주세요.”
// 원본: 회원가입 후 처리 로직 async function handleSignup(user: User) { validateUser(user); await saveToDatabase(user); await sendEmail(user.email, "환영합니다!"); }
TypeScript
복사
Bad: 잘못된 추상화 시도
// 관련 없는 것(저장, 이메일)을 하나로 묶음 function processUserAndSendEmail(user: User) { validateUser(user); saveUser(user); sendWelcomeEmail(user); } // 이메일 로직만 바꾸고 싶은데 전체를 건드려야 함
TypeScript
복사
Good: 잘된 추상화
function saveUser(user: User) { ... } function sendWelcomeEmail(user: User) { ... } // 호출하는 쪽에서 조합 await saveUser(user); await sendWelcomeEmail(user);
TypeScript
복사
본질: “저장”과 “이메일 발송”은 다른 What이에요. 다른 What은 다른 함수로 분리해야 조합이 자유로워요.
다른 변형들: - 너무 좁음: addOne, addTwo 대신 add(a, b) - 너무 넓음: processData(action) 대신 filterData, sortData 분리

자기 점검 체크리스트

코드를 작성하거나 리뷰할 때 체크해보세요.

What/How 분리

이 함수/컴포넌트의 What을 한 문장으로 말할 수 있는가?
이름이 What을 잘 드러내는가?
How가 적절히 숨겨져 있는가?

함정 회피

표면이 아닌 본질을 옮겼는가?
얽힘: 관련 없는 것들이 섞여있지 않은가?
흩어짐: 하나의 기능이 여러 곳에 흩어져있지 않은가?
이른 추상화: 패턴이 세 번 보이기 전에 추상화하지 않았나?
과도한 추상화: 추상화가 복잡함을 더하진 않는가?

정리

사례
What
Form
폼 상태를 관리하고 검증한다
List
비동기 데이터를 불러와서 보여준다
State
상태를 관리한다 (장바구니, 인증 등)
API
API를 호출한다
함정
증상
해결
표면대로 옮김
조건 폭발
본질(What) 찾기
얽힘
관련 없는 것이 섞임
What별로 분리
흩어짐
한 기능이 여러 곳에
한 곳으로 모음
이른 추상화
첫 번째에서 범용화
세 번째 보이면
과도한 추상화
타입 파라미터 3개+
단순하게
잘못된 경계
너무 좁거나/넓거나/엉뚱함
책임 기준으로 분리
다음 장에서는 심화 사례를 다뤄요. 언어를 초월하는 추상화, 플랫폼이 복잡도를 흡수하는 패턴을 볼게요.