제어되는 컴포넌트와 제어되지 않는 컴포넌트를 사용한 폼 상태 복잡성 관리
Min-jun Kim
Dev Intern · Leapcell

소개
끊임없이 진화하는 프론트엔드 개발 환경에서 폼은 사용자 상호작용의 초석으로 남아있습니다. 간단한 로그인 화면부터 복잡한 다단계 데이터 입력 폼까지, 효과적인 상태 관리는 원활하고 예측 가능한 사용자 경험을 위해 매우 중요합니다. "제어되는" 컴포넌트와 "제어되지 않는" 컴포넌트 간의 선택은 폼이 복잡해짐에 따라 이러한 상태 관리에 접근하는 방식을 종종 결정합니다. 각 접근 방식의 미묘한 차이와 실제적인 영향을 이해하는 것은 우리 애플리케이션의 유지보수성, 확장성 및 성능에 상당한 영향을 미칠 수 있습니다. 이 글에서는 제어되는 컴포넌트와 제어되지 않는 컴포넌트의 핵심 개념을 자세히 살펴보고, 복잡한 폼 개발의 맥락에서 이들의 적용과 장단점을 설명합니다.
핵심 개념 및 적용
복잡한 폼 시나리오에 대해 자세히 알아보기 전에, 기본 개념인 제어되는 컴포넌트와 제어되지 않는 컴포넌트를 명확하게 이해하는 것부터 시작하겠습니다.
제어되는 컴포넌트
제어되는 컴포넌트는 React 상태에 의해 완전히 관리되는 폼 요소(예: <input>
, <textarea>
, <select>
)입니다. 모든 상태 변경에는 연관된 핸들러 함수가 필요합니다. React는 입력 상태의 "단일 진실 공급원(single source of truth)"입니다.
원칙:
컴포넌트의 값은 항상 props
에 의해 결정됩니다. 사용자가 입력하거나 상호작용할 때, onChange
핸들러가 컴포넌트의 상태를 업데이트하고, 이는 다시 입력의 value
prop을 업데이트합니다.
구현 예시:
간단한 텍스트 입력을 고려해 봅시다:
import React, { useState } from 'react'; function ControlledTextInput() { const [value, setValue] = useState(''); const handleChange = (event) => { setValue(event.target.value); }; return ( <div> <label htmlFor="controlledInput">Controlled Input:</label> <input type="text" id="controlledInput" value={value} onChange={handleChange} /> <p>Current Value: {value}</p> </div> ); } export default ControlledTextInput;
복잡한 폼을 위한 장점:
- 즉각적인 유효성 검사 및 피드백: 각 키 입력마다 유효성 검사 로직이 실행될 수 있어 사용자에게 즉각적인 피드백을 제공합니다.
- 쉬운 재설정 및 미리 채우기: 단순히 상태를 업데이트하는 것만으로 폼을 초기값으로 쉽게 재설정하거나 데이터를 미리 채울 수 있습니다.
- 중앙 집중식 상태 관리: 모든 폼 데이터는 컴포넌트의 상태에 있게 되어, 애플리케이션의 다른 부분과 쉽게 액세스하고 조작하며 동기화할 수 있습니다.
- 조건부 필드 렌더링: 모든 폼 데이터에 직접 액세스할 수 있으므로 다른 필드 값에 따라 필드를 표시하거나 숨기는 것이 간단해집니다.
제어되지 않는 컴포넌트
제어되지 않는 컴포넌트는 DOM 자체에 의해 값이 관리되는 폼 요소입니다. 일반적으로 ref
를 사용하여 필요할 때 현재 값을 액세스합니다. React는 입력 값을 직접 결정하지 않습니다.
원칙:
이 컴포넌트는 전통적인 HTML 폼 요소처럼 동작합니다. 해당 값이 변경된 후 DOM을 쿼리하여 현재 값을 액세스합니다.
구현 예시:
useRef
훅을 사용하는 입력:
import React, { useRef } from 'react'; function UncontrolledTextInput() { const inputRef = useRef(null); const handleSubmit = (event) => { event.preventDefault(); alert(`Current Value: ${inputRef.current.value}`); }; return ( <form onSubmit={handleSubmit}> <label htmlFor="uncontrolledInput">Uncontrolled Input:</label> <input type="text" id="uncontrolledInput" defaultValue="Initial Value" // value 대신 defaultValue 사용 ref={inputRef} /> <button type="submit">Submit</button> </form> ); } export default UncontrolledTextInput;
복잡한 폼을 위한 장점:
- 간단한 폼에 더 쉬움: 제출 시 값만 필요로 하는 매우 간단한 폼의 경우, 제어되지 않는 컴포넌트는
onChange
핸들러 및 상태 관리 필요성을 줄여 코드를 단순화할 수 있습니다. - 잠재적으로 더 나은 성능: 입력 값이 매우 자주 변경되고 실시간 유효성 검사 또는 동기화가 필요하지 않은 시나리오에서는 각 키 입력마다 상태 업데이트를 피하는 것이 약간의 성능 우위를 제공할 수 있습니다(그러나 최신 React에서는 종종 무시할 수 있습니다).
- 타사 DOM 라이브러리와의 통합: DOM을 직접 조작하는 라이브러리(예: 특정 레거시 날짜 선택기 또는 풍부한 텍스트 편집기)와 통합할 때 제어되지 않는 컴포넌트가 더 쉽게 작동할 수 있습니다.
복잡한 폼에 적용
복잡한 폼은 종종 수많은 필드, 동적 섹션, 복잡한 유효성 검사 규칙 및 필드 간의 상호 의존성을 포함합니다.
복잡한 폼에서의 제어되는 컴포넌트:
다단계 등록 폼의 경우:
import React, { useState } from 'react'; function ComplexRegistrationForm() { const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', password: '', confirmPassword: '', newsletter: false, country: 'USA', }); const [errors, setErrors] = useState({}); const handleChange = (e) => { const { name, value, type, checked } = e.target; setFormData((prevData) => ({ ...prevData, [name]: type === 'checkbox' ? checked : value, })); // 실시간 유효성 검사 (간소화) if (errors[name]) { setErrors((prevErrors) => ({ ...prevErrors, [name]: '', // 사용자가 입력하기 시작하면 오류 지우기 })); } }; const validateForm = () => { let newErrors = {}; if (!formData.firstName) newErrors.firstName = 'First name is required.'; if (!formData.email.includes('@')) newErrors.email = 'Invalid email address.'; if (formData.password.length < 6) newErrors.password = 'Password must be at least 6 characters.'; if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'Passwords do not match.'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }; const handleSubmit = (e) => { e.preventDefault(); if (validateForm()) { console.log('Form Submitted:', formData); // API 호출 또는 추가 로직으로 진행 } else { console.log('Form has validation errors.'); } }; return ( <form onSubmit={handleSubmit} className="complex-form"> <h2>Registration Form</h2> <div className="form-group"> <label htmlFor="firstName">First Name:</label> <input type="text" id="firstName" name="firstName" value={formData.firstName} onChange={handleChange} /> {errors.firstName && <p className="error">{errors.firstName}</p>} </div> <div className="form-group"> <label htmlFor="email">Email:</label> <input type="email" id="email" name="email" value={formData.email} onChange={handleChange} /> {errors.email && <p className="error">{errors.email}</p>} </div> {/* ... password, confirm password, newsletter, country 등의 다른 필드 */} <button type="submit">Register</button> </form> ); } export default ComplexRegistrationForm;
이 예시에서 모든 폼 데이터는 단일 formData
상태 객체에 보관됩니다. 이를 통해 다음이 가능합니다:
- 중앙 집중식 유효성 검사:
validateForm
함수는 모든 필드에 쉽게 액세스하여 필드 간 유효성 검사를 수행할 수 있습니다. - 동적 UI:
confirmPassword
와 같은 필드는password
를 기반으로 조건부로 렌더링되거나 유효성을 검사할 수 있습니다. - 폼 라이브러리와의 쉬운 통합: Formik 또는 React Hook Form(후자는 주로 성능을 위해 제어되지 않는 방식을 사용하는 데 중점을 두지만, 제어되는 래퍼를 제공함)과 같은 라이브러리는 종종 이 제어되는 접근 방식을 활용하거나 이와 잘 작동하는 API를 제공합니다.
복잡한 폼에서의 제어되지 않는 컴포넌트:
(전적으로) 복잡한 폼에서는 덜 일반적이지만, 제어되지 않는 컴포넌트는 특정 요소에 유용하거나, 파일 업로드와 같이 성능이 중요한 입력 또는 각 입력에 대해 부모 컴포넌트가 다시 렌더링되는 것이 비용이 많이 들 수 있는 매우 큰 목록 내의 입력에 useRef
를 결합할 때 유용할 수 있습니다.
import React, { useRef } from 'react'; function UncontrolledFileUpload() { const fileInputRef = useRef(null); const commentRef = useRef(null); // 간단한 제어되지 않는 텍스트 입력 const handleSubmit = (event) => { event.preventDefault(); const files = fileInputRef.current.files; const comment = commentRef.current.value; console.log('Uploaded Files:', files); console.log('Comment:', comment); // 파일 및 댓글 업로드 로직 }; return ( <form onSubmit={handleSubmit}> <h2>Uncontrolled File Upload</h2> <div className="form-group"> <label htmlFor="fileUpload">Upload Files:</label> <input type="file" id="fileUpload" name="fileUpload" multiple ref={fileInputRef} /> </div> <div className="form-group"> <label htmlFor="comment">Your Comment:</label> <textarea id="comment" name="comment" defaultValue="Add your thoughts..." ref={commentRef} ></textarea> </div> <button type="submit">Upload</button> </form> ); } export default UncontrolledFileUpload;
이 경우 file
입력은 기본적으로 제어되지 않습니다(React에서 해당 값의 상태를 쉽게 관리하지 않음). 텍스트 영역의 경우 제출 시 해당 값을 단순히 가져옵니다. 이 접근 방식은 종종 React Hook Form과 같은 특정 라이브러리와 함께 사용되며, 이는 성능을 위해 제어되지 않는 입력 전략을 촉진하고 수동 useRef
관리를 추상화합니다.
하이브리드 접근 방식 및 폼 라이브러리:
실제로는 많은 복잡한 폼에서 하이브리드 접근 방식이나 전용 폼 라이브러리 사용이 도움이 됩니다. React Hook Form과 같은 라이브러리는 종종 성능을 위해 내부적으로 제어되지 않는 컴포넌트를 활용하여 개발자가 입력을 등록하고 제출 시 값을 효율적으로 검색할 수 있도록 하는 동시에 강력한 유효성 검사 및 오류 처리 기능을 제공합니다. 이를 통해 개발자는 제어되지 않는 입력의 성능 이점과 구조화된 폼 라이브러리의 개발 경험 이점이라는 두 가지 장점을 모두 얻을 수 있습니다.
결론
제어되는 컴포넌트와 제어되지 않는 컴포넌트 간의 선택은, 특히 복잡한 폼 개발에서는 엄격한 하나 또는 다른 결정이 아닙니다. 제어되는 컴포넌트는 탁월한 제어, 실시간 유효성 검사 및 예측 가능한 상태 동기화를 제공하여 복잡한 로직과 풍부한 사용자 경험을 요구하는 폼에 이상적입니다. 반면에 제어되지 않는 컴포넌트는 격리된 입력에 대한 단순성을 제공하며 특정 틈새 시나리오에서 성능 이점을 제공할 수 있습니다. 궁극적으로 두 패러다임을 모두 이해하고 적용 시점, 종종 강력한 폼 관리 라이브러리로 보강하는 것이 고품질의 유지보수 가능하고 사용자 친화적인 복잡한 폼을 구축하는 기반을 형성합니다.