
2026-09-10: 설명과 코드 예시를 보완했습니다. 아래 예시는 글의 설계 의도를 전달하기 위한 것이며, 원 프로젝트에 반영된 변경 내역과는 구분합니다.
회원가입이나 날짜 입력은 같은 항목을 PC에서는 한 화면에, 모바일에서는 여러 단계로 보여줄 수 있다. 입력 항목 자체를 중복 작성하지 않고 표시 방식만 바꾸고 싶어 Step 컴포넌트를 만들었다.
step은 현재 단계의 0부터 시작하는 번호다. showAll이 true이면 모든 단계를 함께 보여준다. 이전 이름인 release는 배포 환경과 혼동될 수 있어 표현 목적이 드러나는 이름으로 바꿨다.
import { Children, type PropsWithChildren } from 'react';
import { AnimatePresence, motion, useReducedMotion } from 'framer-motion';
type StepProps = PropsWithChildren<{ step: number; showAll?: boolean }>;
export function Step({ children, step, showAll = false }: StepProps) {
const reduceMotion = useReducedMotion();
const steps = Children.toArray(children);
if (showAll) return <>{children}</>;
if (!Number.isInteger(step) || step < 0 || step >= steps.length) return null;
return (
<AnimatePresence mode="wait" initial={false}>
<motion.div
key={step}
initial={{ opacity: 0, x: reduceMotion ? 0 : 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: reduceMotion ? 0 : -20 }}
transition={{ duration: reduceMotion ? 0 : 0.3 }}
>
{steps[step]}
</motion.div>
</AnimatePresence>
);
}
Children.toArray는 children을 순회하기 쉽게 만들지만, 컴포넌트가 나중에 렌더링하는 내부나 Fragment 안까지 단계로 펼쳐주지는 않는다. 각 단계를 직접 자식으로 전달하는 계약이 필요하다. React Children
이 컴포넌트는 보여주는 단계를 고른다. 이전 단계가 언마운트되면 그 안의 로컬 상태는 사라질 수 있으므로 폼 데이터는 상위 상태나 폼 모델에서 보관해야 한다.
showAll은 애니메이션만 끄는 옵션이 아니다. 모든 항목을 보여주는 별도의 표현 모드다. 화면 크기에 따라 이 값을 바꾼다면 전환 시의 상태 보존과 SSR 초기 화면도 확인해야 한다.
단계 이동 후 포커스를 어디로 보낼지, 오류가 있는 이전 단계로 어떻게 돌아갈지도 사용자 흐름에서 정해야 한다. 작은 Step 컴포넌트가 유효성 검사와 전체 폼 상태까지 맡을 필요는 없다.
이 구조로 얻고 싶은 것은 하나의 UI 정의를 상황에 맞게 보여주는 유연함이다. 표시 방식과 입력 데이터의 책임을 나누면 적용 조건도 더 분명해진다.
Assisted by AI