2023. 11. 26. 23:19ㆍreact
Controlled Props Pattern
리액트에서 컴포넌트를 작성할 때 useState
만을 사용하여 상태관리를 하게 되면 컴포넌트 외부에서는 상태에 전혀 관여할 수 없기 때문에 비제어(Uncontrolled) 컴포넌트라고 부릅니다. 그리고 반대로 props
를 통해 상태값과 상태값을 다루는 콜백함수들을 전달받아 컴포넌트 외부에서 상태를 제어할 수 있도록 하는 것을 Controlled Props Pattern이라고 하며 이런 컴포넌트를 제어(Controlled) 컴포넌트라고 부릅니다.
비제어 컴포넌트는 오직 사전에 정의된 대로만 동작하고 외부 사용자가 임의로 상태를 제어할 수 없습니다. 반면 제어 컴포넌트는 외부에 컴포넌트 제어권을 넘겨주는 게 가능하므로(IOC, Inversion Of Control) 사용자가 좀 더 유연성 있게 사용할 수 있고, 상태를 한 곳에서 관리하는 SSOT(Single Source Of Truth)가 가능해집니다.
// Uncontrolled - 내부 state만 사용
function UncontrolledCounter() {
const [count, setCount] = useState(0)
function onIncrement() { setCount((prev) => (prev += 1)) }
function onDecrement() { setCount((prev) => (prev -= 1)) }
return (
<div>
<input value={count} />
<button onClick={onDecrement}>{"-"}</button>
<button onClick={onIncrement}>{"+"}</button>
</div>
)
}
// Controlled - 외부 props 사용
function ControlledCounter({count, onIncrement, onDecrement}) {
return (
<div>
<input value={count} />
<button onClick={onDecrement}>{"-"}</button>
<button onClick={onIncrement}>{"+"}</button>
</div>
)
}
function App() {
const [count, setCount] = useState(0)
function onIncrement() { setCount((prev) => (prev += 1)) }
function onDecrement() { setCount((prev) => (prev -= 1)) }
return (
<div>
{/* 내부에서 정의한대로만 동작 */}
<UncontrolledCounter />
{/* 외부에서 정의한대로 동작 */}
<ControlledCounter count={count} onIncrement={onIncrement} onDecrement={onDecrement}/>
<div/>
)
}
useControlled hook
리액트 기본 요소인 <input/>
이나 <textarea/>
와 같은 컴포넌트들을 살펴보면 props
를 전달하지 않아도 내부 상태를 사용하여 동작하면서도, props
를 통해 외부에서 상태 관리가 가능한 제어 컴포넌트임을 알 수 있습니다.
function App() {
const [value, setValue] = useState("")
return (
<div>
{/* value 프로퍼티가 없어도 잘 동작함(uncontrolled) */}
<input onChange={event => console.log(event.target.value)} />
{/* value 프로퍼티를 전달하여 외부에서 제어 가능(controlled) */}
<input value={value} onChange={event => setValue(event.target.value)}/>
</div>
)
}
이처럼 기본적으로는 내부의 useState
를 사용하다가, 외부에서 props
를 전달하면 해당 값을 대신 사용하도록 hook
을 만들어서 사용하면, 상황에 따라 훨씬 더 유연하게 사용될 수 있는 컴포넌트를 작성할 수 있습니다.
export function useControlled(prop, initialState) {
const { current: isInitControlled } = useRef(prop !== undefined && prop !== null)
const [state, setState] = useState(initialState)
// -------
// controlled, uncontrolled 상태가 변경되면 경고
// useEffect(()=>{
// const isControlled = prop !== undefined && prop !== null
// if (isInitControlled !== isControlled) {
// console.error("컴포넌트가 controlled, uncontrolled상태가 바뀌면 사이드이펙트가 발생할 수 있습니다.")
// }
// }, [isInitControlled, state, prop])
// -------
// controleld 상태일 경우 prop을 사용하고, uncontrolled 상태일 경우 state를 사용
const isControlled = prop !== undefined && prop !== null
const controlledValue = isControlled ? prop : state
function setStateControlled(newState) {
// controlled 상태가 아닌경우에만 setState호출
if (!isControlled) {
setState(newState)
}
}
return [controlledValue, setStateControlled]
}
(주석처리된 부분은 controlled 상태와 uncontrolled 상태가 왔다 갔다 하는 경우에는 예상치 못한 사이드이펙트를 발생시킬 수 있으므로, 이와 관련한 경고 알림 노출입니다.)
export interface ICounterProps {
count?: number
onIncrement?: () => void
onDecrement?: () => void
}
const Counter = function ({ count, onIncrement, onDecrement }: ICounterProps) {
// useControlled hook 사용
const [count, setCount] = useControlled(count, 0)
function onClickIncrement() {
setCount((prev) => prev += 1)
onIncrement?.()
}
function onClickDecrement() {
setCount((prev) => prev -= 1)
onDecrement?.()
}
return (
<div>
<input value={count} />
<button onClick={onClickDecrement}>{"-"}</button>
<button onClick={onClickIncrement}>{"+"}</button>
</div>
)
}
const App = function() {
const [count, setCount] = useState(0)
return (
<div>
{/* Uncontrolled Counter */}
<Counter/>
{/* Controlled Counter */}
<Counter count={count} onClickPlus={()=>setCount(prev=>prev+=2)}/>
</div>
)
}
'react' 카테고리의 다른 글
React event의 여러가지 target (0) | 2023.11.19 |
---|---|
box-shadow로 inline border 만들기 (0) | 2023.11.13 |
CSS BoxShadow 제대로 알기 (0) | 2023.11.06 |
html input 유효성(validity) 검사 (2) | 2023.10.14 |
input요소 checkValidity와 reportValidity (0) | 2023.10.13 |