파라미터 vs 객체
같은 기능, 다른 API
Section titled “같은 기능, 다른 API”같은 기능을 하는 훅도 API 설계 방식에 따라 사용성이 달라진다.
// 파라미터 방식export const useAppForeground = (onForeground: () => void) => { // 구현...};
// 객체 방식export const useAppForeground = ({ onForeground,}: { onForeground: () => void;}) => { // 구현...};사용할 때도 차이가 난다.
// 파라미터 방식useAppForeground(() => refetch());
// 객체 방식useAppForeground({ onForeground: () => refetch() });어느 쪽이 더 나을까?
파라미터 방식의 장점
Section titled “파라미터 방식의 장점”1. 간결함
Section titled “1. 간결함”// 파라미터: 짧고 명확useAppForeground(() => refetch());
// 객체: 더 길다useAppForeground({ onForeground: () => refetch() });타이핑해야 할 코드가 적다. 특히 자주 사용하는 유틸리티 함수나 훅이라면, 간결함이 중요하다.
2. 타입 선언 비용이 낮다
Section titled “2. 타입 선언 비용이 낮다”// 파라미터: 별도 타입 구조 불필요const useAppForeground = (callback: () => void) => { // 콜백 시그니처가 바로 드러남};
// 객체: 타입 구조 정의 필요interface UseAppForegroundProps { onForeground: () => void;}const useAppForeground = (props: UseAppForegroundProps) => { // 인터페이스 또는 인라인 타입 정의 필요};타입 추론 자체는 두 방식 모두 잘 동작하지만, 파라미터 방식은 별도의 타입 구조를 만들 필요가 없어 선언 비용이 낮다.
객체 방식의 장점
Section titled “객체 방식의 장점”1. 명시성 (가독성)
Section titled “1. 명시성 (가독성)”가장 큰 장점은 명시성이다.
// 6개월 뒤 코드를 다시 볼 때useAppForeground({ onForeground: handleSomething }); // 명확useAppForeground(handleSomething); // 뭐 하는 거지?특히 함수 이름이 handleForeground가 아니라 fetchData나 updateUI 같은 일반적인 이름이면 객체 방식이 훨씬 명확하다.
코드를 읽는 사람에게 컨텍스트를 제공한다.
- “이 함수가 무엇을 하는가?”보다
- “이 함수가 언제 실행되는가?”를 바로 알 수 있다
// 이 코드만 봐도useAppForeground({ onForeground: refetch });// "아, 포그라운드로 전환될 때 refetch가 실행되는구나"를 즉시 이해
// 이 코드는useAppForeground(refetch);// 훅 이름을 보고 추론해야 함
// 오히려 위의 코드보다useRefetchOnForeground();// 로 한단계 더 감싼 훅으로 캡슐화가 더 나을 것 같음2. 확장성
Section titled “2. 확장성”나중에 옵션을 추가하기 쉽다.
// 객체: 새 옵션 추가가 자연스러움useAppForeground({ onForeground: () => refetch(), debounce: 300, // 새로운 옵션 onBackground: () => {}, // 새로운 콜백});
// 파라미터: 시그니처 변경 필요useAppForeground( () => refetch(), { debounce: 300, onBackground: () => {} } // 두 번째 파라미터 추가);객체 방식은 기존 사용처의 코드를 수정하지 않고도 새 옵션을 추가할 수 있다.
3. 순서 무관
Section titled “3. 순서 무관”// 객체: 순서 상관없음useAppForeground({ debounce: 300, onForeground: handleForeground, enabled: true,});
// 파라미터: 순서 중요useAppForeground(handleForeground, 300, true); // 순서를 기억해야 함파라미터가 많아질수록 순서를 기억하기 어렵다. 객체는 이름으로 식별하므로 순서가 중요하지 않다.
4. 선택적 파라미터 처리가 명확
Section titled “4. 선택적 파라미터 처리가 명확”// 객체: 어떤 옵션이 생략됐는지 명확useAppForeground({ onForeground: handleForeground, // debounce는 생략});
// 파라미터: undefined를 명시적으로 전달해야 함useAppForeground(handleForeground, undefined, true); // debounce 생략하려면 undefined객체 방식의 단점
Section titled “객체 방식의 단점”1. 참조 안정성 & 리렌더링 이슈
Section titled “1. 참조 안정성 & 리렌더링 이슈”// 매 렌더마다 새로운 객체가 생성됨useAppForeground({ onForeground: refetch, debounce: 300,});이 패턴은 내부 구현에 따라.
- 매 렌더마다 객체가 새로 생성됨
useEffectdependency에 사용하면 불필요한 effect 재실행 위험useMemo/useCallback을 강제할 수 있음
// 안정적인 참조를 위해 추가 작업 필요const options = useMemo( () => ({ onForeground: refetch, debounce: 300, }), [refetch]);
useAppForeground(options);→ 따라서 객체 방식은 구현 난이도가 더 높다
2. 보일러플레이트 증가
Section titled “2. 보일러플레이트 증가”타입 정의, 구조 분해, 옵션 객체 생성 등 코드량이 늘어난다. 간단한 유틸리티 함수에 객체 방식을 쓰면 오히려 가독성이 떨어질 수 있다.
실제 라이브러리들은 어떻게 설계했을까
Section titled “실제 라이브러리들은 어떻게 설계했을까”복잡한 설정 → 객체
Section titled “복잡한 설정 → 객체”TanStack Query (React Query)
// 옵션이 많고 복잡함 → 객체useQuery({ queryKey: ["todos"], queryFn: fetchTodos, enabled: true, staleTime: 5000, refetchOnWindowFocus: false, // ... 수십 개의 옵션});
useMutation({ mutationFn: updateTodo, onSuccess: () => {}, onError: () => {}, // ...});React Hook Form
// 폼 설정이 복잡함 → 객체const { register, handleSubmit } = useForm({ defaultValues: { name: "" }, mode: "onChange", resolver: zodResolver(schema), // ...});Formik
// 폼 로직이 복잡함 → 객체const formik = useFormik({ initialValues: {}, onSubmit: () => {}, validate: () => {}, // ...});패턴: 설정이 복잡하고 옵션이 많으면 → 객체
간단한 상태/값 → 파라미터
Section titled “간단한 상태/값 → 파라미터”Zustand
// selector만 전달 → 파라미터const bears = useStore((state) => state.bears);
// 단일 atom → 파라미터const [value, setValue] = useAtom(countAtom);Jotai
// atom만 전달 → 파라미터const [count, setCount] = useAtom(countAtom);
// 초기값만 필요 → 파라미터const countAtom = atom(0);패턴: 필수 파라미터 1~2개만 있으면 → 파라미터
필수 + 옵션 조합 → 혼합
Section titled “필수 + 옵션 조합 → 혼합”SWR
// 필수(key, fetcher) + 선택적 옵션 → 혼합useSWR("/api/user", fetcher, { refreshInterval: 1000, revalidateOnFocus: false, // ...});React Router
// 필수(path) + 선택적 옵션 → 혼합navigate("/path", { replace: true, state: { from: "home" },});React Hook Form의 register
// 필수(name) + 선택적 옵션 → 혼합register("email", { required: true, pattern: /\S+@\S+\.\S+/,});패턴: 필수 파라미터가 명확하고 + 선택적 옵션이 있으면 → 혼합
라이브러리 분석
Section titled “라이브러리 분석”1. 필수 파라미터가 1개 → 파라미터
Section titled “1. 필수 파라미터가 1개 → 파라미터”✅ const useAtom = (atom) => { /* ... */ }✅ const useToggle = (initialValue) => { /* ... */ }✅ const useStore = (selector) => { /* ... */ }예: Jotai, Zustand
2. 옵션이 많아지기 시작하면 → 객체
Section titled “2. 옵션이 많아지기 시작하면 → 객체”✅ const useQuery = ({ queryKey, queryFn, enabled, staleTime, ... }) => { /* ... */ }✅ const useForm = ({ defaultValues, mode, resolver, ... }) => { /* ... */ }시그니처를 한 번에 기억하기 어려워지는 순간이 객체 전환 시점이다. (보통 옵션이 4~5개를 넘어가면서부터)
예: React Query, React Hook Form
3. 필수 1~2개 + 선택적 옵션 → 혼합
Section titled “3. 필수 1~2개 + 선택적 옵션 → 혼합”✅ const useSWR = (key, fetcher, options?) => { /* ... */ }✅ const navigate = (to, options?) => { /* ... */ }✅ const register = (name, options?) => { /* ... */ }예: SWR, React Router, React Hook Form
4. 확장 가능성이 높음 → 객체 (처음부터)
Section titled “4. 확장 가능성이 높음 → 객체 (처음부터)”// 나중에 옵션이 추가될 것 같으면 처음부터 객체✅ const useAppForeground = ({ onForeground, debounce?, throttle? }) => { /* ... */ }
// 파라미터로 시작했다가 나중에 리팩토링하면 Breaking Change❌ const useAppForeground = (onForeground, debounce?, throttle?) => { /* ... */ }예: 공용 라이브러리, 팀 공유 유틸
실전 가이드
Section titled “실전 가이드”시작은 파라미터? 아니면 객체?
Section titled “시작은 파라미터? 아니면 객체?”라이브러리 분석 결과, 명확한 패턴이 있다.
내부 유틸/간단한 훅 → 파라미터로 시작
// Zustand, Jotai처럼const useToggle = (initial: boolean) => { /* ... */};const useDebounce = (value: string, delay: number) => { /* ... */};공용 API/확장 예상 → 처음부터 객체
// React Query, React Hook Form처럼const useAppForeground = ({ onForeground, debounce = 0,}: { onForeground: () => void; debounce?: number;}) => { /* ... */};Breaking Change를 피하려면, 확장 가능성을 미리 고려해야 한다.
혼합 접근법: 필수 파라미터 + 옵션 객체
Section titled “혼합 접근법: 필수 파라미터 + 옵션 객체”SWR, React Router 패턴을 따르는 방식이다.
// SWR 방식const useAppForeground = ( onForeground: () => void, // 필수 options?: { // 선택적 옵션 debounce?: number; throttle?: number; }) => { // 구현...};
// 사용useAppForeground(refetch); // 간단한 경우useAppForeground(refetch, { debounce: 300 }); // 옵션 필요한 경우이 방식의 장점:
- 간단한 사용 케이스는 간결하게 (
useSWR(key, fetcher)) - 복잡한 사용 케이스는 명시적으로 (
useSWR(key, fetcher, options)) - Breaking Change 없이 확장 가능
하지만 혼합 방식은:
- 타입 정의(overload)가 복잡해질 수 있음
- TypeScript inference가 꼬이기 쉬움
- API 문서가 길어짐
→ 사용성은 좋지만, 타입 정의와 유지보수 난이도는 가장 높다.
실전 선택 플로우
Section titled “실전 선택 플로우”라이브러리 패턴을 바탕으로 한 선택 플로우.
필수 파라미터가 1개일까? └─ YES → 파라미터 (Jotai, Zustand 패턴) └─ 나중에 옵션 추가될 것 같을까? └─ YES → 혼합 방식 고려 (SWR 패턴) └─ NO → 파라미터 유지
└─ NO → 시그니처를 외우기 어려울까? └─ YES → 객체 (React Query 패턴) └─ NO → 2~4개 정도일까? └─ 확장 가능성 높을까? → 객체 (React Hook Form 패턴) └─ 간단한 내부 유틸일까? → 혼합 (SWR 패턴)구체적 예시
Section titled “구체적 예시”1. 간단한 내부 훅 → 파라미터
// Jotai처럼✅ const useToggle = (initial: boolean) => { /* ... */ }✅ const useCounter = (initial: number) => { /* ... */ }2. 필수 + 옵션 → 혼합
// SWR처럼✅ const useDebounce = (value: string, options?: { delay: number }) => { /* ... */ }✅ const useInterval = (callback: () => void, options?: { delay: number }) => { /* ... */ }3. 복잡한 설정 → 객체
// React Query처럼✅ const useQuery = ({ queryKey, queryFn, enabled, ... }: Options) => { /* ... */ }✅ const useForm = ({ defaultValues, mode, resolver, ... }: Options) => { /* ... */ }4. 공용 라이브러리 → 객체 (처음부터)
// 나중에 옵션 추가될 것 확실하면✅ const useAppForeground = ({ onForeground, debounce? }: Options) => { /* ... */ }
// Breaking Change 없이 확장 가능정답은 없다. 트레이드오프를 이해하고 상황에 맞게 선택하면 된다.
| 기준 | 파라미터 | 객체 |
|---|---|---|
| 간결함 | ✅ | ❌ |
| 명시성 | ❌ | ✅ |
| 확장성 | ❌ | ✅ |
| 타입 선언 비용 | ✅ | ⚠️ |
| 순서 무관 | ❌ | ✅ |
| 구현 난이도 | ✅ | ⚠️ |
앞으로 API를 설계할 때, 다음을 고민해보면 좋다.
1. 성공한 라이브러리의 패턴을 참고해보자
Section titled “1. 성공한 라이브러리의 패턴을 참고해보자”- 간단한 상태 관리 → Zustand, Jotai 패턴 (파라미터)
- 복잡한 설정 → React Query, React Hook Form 패턴 (객체)
- 필수 + 옵션 → SWR, React Router 패턴 (혼합)
2. 프로젝트 상황에 맞춰 선택해보자
Section titled “2. 프로젝트 상황에 맞춰 선택해보자”- 내부 유틸: 간결함 우선 → 파라미터
- 팀 공유 라이브러리: 명시성 우선 → 객체
- 확장 예상: Breaking Change 방지 → 객체 또는 혼합
3. 일관성을 유지하면 좋다
Section titled “3. 일관성을 유지하면 좋다”프로젝트 내에서 비슷한 상황에서는 비슷한 패턴을 사용하는 게 좋다. 기존에 사용 중인 라이브러리 스타일과 일관성을 유지하면, 팀원들이 API를 더 쉽게 이해할 수 있다.
4. 트레이드오프를 이해하고 선택하면 된다
Section titled “4. 트레이드오프를 이해하고 선택하면 된다”- 파라미터: 간결함 ↔ 확장성 약함
- 객체: 명시성, 확장성 ↔ 구현 복잡도, 참조 안정성 이슈
- 혼합: 유연성 ↔ 타입 정의 복잡도
정답은 없다. 상황에 맞는 최선의 선택을 하면 된다.