Skip to content

파라미터 vs 객체

같은 기능을 하는 훅도 API 설계 방식에 따라 사용성이 달라진다.

// 파라미터 방식
export const useAppForeground = (onForeground: () => void) => {
// 구현...
};
// 객체 방식
export const useAppForeground = ({
onForeground,
}: {
onForeground: () => void;
}) => {
// 구현...
};

사용할 때도 차이가 난다.

// 파라미터 방식
useAppForeground(() => refetch());
// 객체 방식
useAppForeground({ onForeground: () => refetch() });

어느 쪽이 더 나을까?

// 파라미터: 짧고 명확
useAppForeground(() => refetch());
// 객체: 더 길다
useAppForeground({ onForeground: () => refetch() });

타이핑해야 할 코드가 적다. 특히 자주 사용하는 유틸리티 함수나 훅이라면, 간결함이 중요하다.

// 파라미터: 별도 타입 구조 불필요
const useAppForeground = (callback: () => void) => {
// 콜백 시그니처가 바로 드러남
};
// 객체: 타입 구조 정의 필요
interface UseAppForegroundProps {
onForeground: () => void;
}
const useAppForeground = (props: UseAppForegroundProps) => {
// 인터페이스 또는 인라인 타입 정의 필요
};

타입 추론 자체는 두 방식 모두 잘 동작하지만, 파라미터 방식은 별도의 타입 구조를 만들 필요가 없어 선언 비용이 낮다.

가장 큰 장점은 명시성이다.

// 6개월 뒤 코드를 다시 볼 때
useAppForeground({ onForeground: handleSomething }); // 명확
useAppForeground(handleSomething); // 뭐 하는 거지?

특히 함수 이름이 handleForeground가 아니라 fetchDataupdateUI 같은 일반적인 이름이면 객체 방식이 훨씬 명확하다.

코드를 읽는 사람에게 컨텍스트를 제공한다.

  • “이 함수가 무엇을 하는가?”보다
  • “이 함수가 언제 실행되는가?”를 바로 알 수 있다
// 이 코드만 봐도
useAppForeground({ onForeground: refetch });
// "아, 포그라운드로 전환될 때 refetch가 실행되는구나"를 즉시 이해
// 이 코드는
useAppForeground(refetch);
// 훅 이름을 보고 추론해야 함
// 오히려 위의 코드보다
useRefetchOnForeground();
// 로 한단계 더 감싼 훅으로 캡슐화가 더 나을 것 같음

나중에 옵션을 추가하기 쉽다.

// 객체: 새 옵션 추가가 자연스러움
useAppForeground({
onForeground: () => refetch(),
debounce: 300, // 새로운 옵션
onBackground: () => {}, // 새로운 콜백
});
// 파라미터: 시그니처 변경 필요
useAppForeground(
() => refetch(),
{ debounce: 300, onBackground: () => {} } // 두 번째 파라미터 추가
);

객체 방식은 기존 사용처의 코드를 수정하지 않고도 새 옵션을 추가할 수 있다.

// 객체: 순서 상관없음
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
// 매 렌더마다 새로운 객체가 생성됨
useAppForeground({
onForeground: refetch,
debounce: 300,
});

이 패턴은 내부 구현에 따라.

  • 매 렌더마다 객체가 새로 생성됨
  • useEffect dependency에 사용하면 불필요한 effect 재실행 위험
  • useMemo / useCallback을 강제할 수 있음
// 안정적인 참조를 위해 추가 작업 필요
const options = useMemo(
() => ({
onForeground: refetch,
debounce: 300,
}),
[refetch]
);
useAppForeground(options);

→ 따라서 객체 방식은 구현 난이도가 더 높다

타입 정의, 구조 분해, 옵션 객체 생성 등 코드량이 늘어난다. 간단한 유틸리티 함수에 객체 방식을 쓰면 오히려 가독성이 떨어질 수 있다.

실제 라이브러리들은 어떻게 설계했을까

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: () => {},
// ...
});

패턴: 설정이 복잡하고 옵션이 많으면 → 객체

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개만 있으면 → 파라미터

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+/,
});

패턴: 필수 파라미터가 명확하고 + 선택적 옵션이 있으면 → 혼합

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?) => { /* ... */ }

예: 공용 라이브러리, 팀 공유 유틸

라이브러리 분석 결과, 명확한 패턴이 있다.

내부 유틸/간단한 훅 → 파라미터로 시작

// 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 문서가 길어짐

사용성은 좋지만, 타입 정의와 유지보수 난이도는 가장 높다.

라이브러리 패턴을 바탕으로 한 선택 플로우.

필수 파라미터가 1개일까?
└─ YES → 파라미터 (Jotai, Zustand 패턴)
└─ 나중에 옵션 추가될 것 같을까?
└─ YES → 혼합 방식 고려 (SWR 패턴)
└─ NO → 파라미터 유지
└─ NO → 시그니처를 외우기 어려울까?
└─ YES → 객체 (React Query 패턴)
└─ NO → 2~4개 정도일까?
└─ 확장 가능성 높을까? → 객체 (React Hook Form 패턴)
└─ 간단한 내부 유틸일까? → 혼합 (SWR 패턴)

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 방지 → 객체 또는 혼합

프로젝트 내에서 비슷한 상황에서는 비슷한 패턴을 사용하는 게 좋다. 기존에 사용 중인 라이브러리 스타일과 일관성을 유지하면, 팀원들이 API를 더 쉽게 이해할 수 있다.

4. 트레이드오프를 이해하고 선택하면 된다

Section titled “4. 트레이드오프를 이해하고 선택하면 된다”
  • 파라미터: 간결함 ↔ 확장성 약함
  • 객체: 명시성, 확장성 ↔ 구현 복잡도, 참조 안정성 이슈
  • 혼합: 유연성 ↔ 타입 정의 복잡도

정답은 없다. 상황에 맞는 최선의 선택을 하면 된다.