React Pattern - 제어 컴포넌트 패턴(Controlled Props Pattern)

2023. 11. 26. 23:19react

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