[React] Input elements must be either controlled or uncontrolled

wisdom·2022년 6월 5일
1

Troubleshooting

발생한 문제

  • 일기 작성 컴포넌트를 만드는 중 오늘의 일기에서 점수를 매기는 부분을 만들었고 초깃값을 1로 만들고 싶어서 defaultValue 를 1로 두고, value 값을 useState() 에서 가지고 오는 방식으로 만들었는데 해당 오류가 발생했다.

해결 및 시도 방법

  • 콘솔 탭에서 확인할 수 있는 Warning: "contains an input of type number with both value and defaultValue props. Input elements must be either controlled or uncontrolled" 구글링 시도
    • https://github.com/facebook/react/issues/20599
    • 한 개발자가 "React should allow defaultValue and input value to be set in the controlled state." 제목으로 react repository에 이슈 제기를 했고 그에 대한 답변을 보고 왜 에러가 발생한건지 이해를 할 수 있었다.
    • 해당 상황에서 defaultValue를 지정하는 경우, 리액트의 핵심 설계 원리인 "single source of truth" 관점에서 위배되는 상황인 것이다.

Before

let [score, setScore] = useState();
<input 
    type="number" 
    placeholder="score" 
    defaultValue="1" 
    value={score}
    onChange={(e) => setScore(e.target.value)}
/>

After

let [score, setScore] = useState(1);

<input 
    type="number" 
    placeholder="score" 
    value={score}
    onChange={(e) => setScore(e.target.value)}
/>

추가적으로 알게된 내용: 지연 초기화

  • 리액트는 매 렌더링마다 모든 코드를 실행하는데
const [score, setScore] = useState(비싼초기계산값) 

여기서 '비싼초기계산값' 코드가 매 렌더링마다 실행되고, 이미 업데이트된 state값을 들고 있으니 첫 렌더링 이후의 렌더링 값들은 무시된다고 한다.
하지만 아래 처럼 함수를 넘기면

const [score, setScore] = useState(() => get비싼초기계산값());

함수인 () => get비싼초기계산값() 은 매 렌더링마다 불리지만 실제 비싼 계산은 "get비싼초기계산값"이 실행되는 첫 렌더링때만 수행한다고 한다. 즉 값으로 바로 초기화하든, 함수로 지연초기화하든 리액트는 매 렌더링마다 모든 코드를 실행한다. 다만 리액트 문법에 따라 함수로 지연초기화하면 실제 내부함수 실행은 초기렌더링에만 실행한다.

  • 내가 현재 작성한 코드의 경우의 초기 값은 단순한 원시값이다. 함수를 한번만 호출해도 매번 함수를 만드는 비용이 발생할 것이다. 그리고 함수를 만드는 비용이 단순히 값이나 변수를 전달하는 것보다 더 높을 수 있다. 이것은 지나친 최적화라고 볼 수 있다.
  • 그렇다면 언제 지연 초기화를 사용해야 하는가? 상황에 따라 달라진다. 공식문서에서는 '비용이 큰 계산'을 할 때 사용하라고 가이드하고 있다. 예를들어 localStorage에서 값을 읽는 것은 비용이 큰 계산일 것이다. 배열의 .map(), .filter(), .find() 등을 사용하는 것도 비용이 큰 계산일 것이다. 좀 더 쉽게 생각해보자. 만약 값을 얻기 위해 어떤 함수를 호출해야 한다면 비용이 큰 계산일 가능성이 높으며, 이러한 경우 지연 초기화를 사용하면 이득을 볼 수 있을 것 같다.

추가적으로 알게된 내용: Controlled components and Uncontrolled components

  • A Controlled Component is one that takes its current value through props and notifies changes through callbacks like onChange. A parent component "controls" it by handling the callback and managing its own state and passing the new values as props to the controlled component. You could also call this a "dumb component".
  • A Uncontrolled Component is one that stores its own state internally, and you query the DOM using a ref to find its current value when you need it. This is a bit more like traditional HTML.
    Most native React form components support both controlled and uncontrolled usage:
// Controlled:
<input type="text" value={value} onChange={handleChange} />

// Uncontrolled:
<input type="text" defaultValue="foo" ref={inputRef} />
// Use `inputRef.current.value` to read the current value of <input>
  • In most (or all) cases you should use controlled components.

회고

  • "single source of truth" 원칙을 지키는 것이 매우 중요하다!!
  • 컴포넌트를 최적화 하는 방법도 필요할 때마다 찾아보고 내 걸로 만들어야겠다.
  • 리액트 공식 문서에서 폼을 구현할 때 제어 컴포넌트를 활용할 것을 권장하고 있다. 개발을 하다보면 100% 제어 컴포넌트를 활용하지 못할 수도 있겠지만 그리고 그럴 필요가 없을 수도 있지만 아래의 사진을 참고해서 특정한 경우에는 반드시 제어 컴포넌트만을 사용해 구현해야 한다는 걸 배웠다.

profile
문제를 정의하고, 문제를 해결하는

0개의 댓글