Controlled vs Uncontrolled 컴포넌트
React에서 컴포넌트의 상태를 누가 관리하느냐에 따라 Controlled(제어)와 Uncontrolled(비제어) 컴포넌트로 나뉩니다. 이 개념은 폼 입력뿐만 아니라 멀티스텝 플로우, 모달 등 다양한 UI 패턴에 적용됩니다.
Controlled Component (제어 컴포넌트)
섹션 제목: “Controlled Component (제어 컴포넌트)”React의 state로 입력값을 관리하는 방식입니다. 제출 전에 사용자 입력값을 실시간으로 추적하고 대응할 수 있다는 것이 가장 큰 이점입니다.
import { useState, useEffect } from "react";
export const ControlledForm = () => { const [error, setError] = useState<string>(""); const [name, setName] = useState<string>(""); const [age, setAge] = useState<number>();
useEffect(() => { if (name.length < 2) { setError("이름은 2자 이상 입력해주세요."); } else { setError(""); } }, [name]);
return ( <form> <input name="name" type="text" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} /> {error && <p>{error}</p>} <input name="age" type="number" placeholder="Age" value={age} onChange={(e) => setAge(Number(e.target.value))} /> <input type="submit" /> </form> );};Uncontrolled Component (비제어 컴포넌트)
섹션 제목: “Uncontrolled Component (비제어 컴포넌트)”DOM이 직접 입력값을 관리하고, 필요할 때 ref로 값을 읽어오는 방식입니다.
import { FormEvent, useRef } from "react";
export const UncontrolledForm = () => { const nameInputRef = useRef<HTMLInputElement>(null); const ageInputRef = useRef<HTMLInputElement>(null);
const handleSubmit = (e: FormEvent) => { if (nameInputRef.current && ageInputRef.current) { console.log(nameInputRef.current.value); console.log(ageInputRef.current.value); } e.preventDefault(); };
return ( <form onSubmit={handleSubmit}> <input name="name" type="text" placeholder="Name" ref={nameInputRef} /> <input name="age" type="number" placeholder="Age" ref={ageInputRef} /> <input type="submit" /> </form> );};createRef vs useRef
섹션 제목: “createRef vs useRef”createRef는 호출할 때마다 항상 새로운 객체를 반환하며, 클래스 컴포넌트에서 주로 사용useRef는 항상 같은 객체를 반환하며, 함수 컴포넌트에서 권장- 공식 문서에서도
useRef사용을 권장하고 있으므로, 굳이 레거시 코드를 사용할 필요가 없음
Uncontrolled Flow: 멀티스텝 패턴
섹션 제목: “Uncontrolled Flow: 멀티스텝 패턴”회원가입이나 설문조사처럼 한 단계씩 정보를 입력하는 멀티스텝 UI에 사용합니다. 컴포넌트 내부에서 현재 단계를 관리하므로 비제어 방식입니다.
import React, { ReactElement, ReactNode, useState } from "react";
interface Props { children: ReactNode[]; onDone: boolean;}
export const UncontrolledFlow = ({ children, onDone }: Props) => { const [currentStepIndex, setCurrentStepIndex] = useState<number>(0);
const currentChild = React.Children.toArray(children)[ currentStepIndex ] as ReactElement<{ goNext: () => void }>;
const goNext = () => { setCurrentStepIndex((prev) => prev >= React.Children.count(children) - 1 ? 0 : prev + 1 ); };
return ( React.isValidElement(currentChild) && React.cloneElement(currentChild, { goNext }) );};사용 예시:
function App() { return ( <UncontrolledFlow onDone={false}> <StepIndicator step={1} /> <StepIndicator step={2} /> <StepIndicator step={3} /> </UncontrolledFlow> );}Controlled Flow: 외부에서 단계 관리
섹션 제목: “Controlled Flow: 외부에서 단계 관리”부모 컴포넌트가 현재 단계와 데이터를 관리하는 제어 방식입니다.
interface Props { children: ReactNode[]; currentIndex: number; onNext: (data: object) => void;}
export const ControlledFlow = ({ children, currentIndex, onNext }: Props) => { const goNext = (dataFromStep: object) => { onNext(dataFromStep); };
const currentChild = React.Children.toArray(children)[ currentIndex ] as ReactElement<{ goNext: (data: object) => void }>;
return ( React.isValidElement(currentChild) && React.cloneElement(currentChild, { goNext }) );};부모에서 state lifting으로 상태를 관리하면 조건부 단계 추가 같은 유연한 로직이 가능합니다.
function App() { const [data, setData] = useState({ data: 0 }); const [currentStepIndex, setCurrentStepIndex] = useState(0);
const goNext = (dataFromStep: object) => { setData({ ...data, ...dataFromStep }); setCurrentStepIndex(currentStepIndex + 1); };
return ( <ControlledFlow onNext={goNext} currentIndex={currentStepIndex}> <StepIndicator step={1} /> <StepIndicator step={2} /> {data?.data === 3 && <StepIndicator step={3} />} <StepIndicator step={4} /> </ControlledFlow> );}Controlled Modal
섹션 제목: “Controlled Modal”모달도 제어/비제어 패턴을 적용할 수 있습니다. 비제어 모달은 내부에서 열림/닫힘 상태를 관리하고, 제어 모달은 외부에서 관리합니다.
// 제어 모달 - 외부에서 상태 관리type Props = { shouldDisplay: boolean; onClose: () => void; children: ReactNode;};
export const ControlledModal = ({ shouldDisplay, onClose, children }: Props) => { return ( <> {shouldDisplay && ( <div className="modal-background" onClick={onClose}> <div className="modal-content" onClick={(e) => e.stopPropagation()}> <button onClick={onClose}>Close</button> {children} </div> </div> )} </> );};제어 모달은 forwardRef와 useImperativeHandle을 사용해서도 외부에서 제어할 수 있으며, 공통 컴포넌트를 만들 때 자주 사용되는 패턴입니다.
Children API 사용 시 주의점
섹션 제목: “Children API 사용 시 주의점”React 공식 문서에서는 React.Children API 사용에 주의를 권고합니다.
children은 React에서 불분명한 데이터 구조로 취급됨- 내부적으로는 배열로 변환되지만, 자식이 하나면 배열을 생성하지 않음
- 렌더링 결과를 포함하지 않기 때문에 실수가 발생할 수 있음
그래도 children.map 대신 Children.map()과 isValidElement()를 결합하면 좀 더 안정적으로 children을 다룰 수 있습니다.