React 폼 바인딩과 비제어 컴포넌트

0

HTML 폼 엘리먼트는 폼 엘리먼트 자체가 내부 상태를 가지기 때문에, React의 다른 DOM 엘리먼트와는 다르게 동작하는데, 다음의 코드와 같이 순수한 HTML에서 폼은 name을 입력받는데, 이 폼은 사용자가 폼을 제출하면 새로운 페이지로 이동하는 기본 HTML 폼 동작을 수행합니다.

<form>
  <label>
    Name:
    <input type="text" name="name" />
  </label>
  <input type="submit" value="Submit" />
</form>

만약 React에서 동일한 동작을 원하는 경우에는 위의 코드를 그대로 사용해도 되지만, 대부분의 경우에는 JavaScript 함수로 폼의 제출을 처리하고 사용자가 폼에 입력한 데이터에 접근하도록 하는 것이 편리하기 때문에 이런 방식을 많이 사용하는데, 이렇게 JavaScript 함수로 폼의 제출을 처리하는 표준 방식은 “제어 컴포넌트”라고 불리는 기술입니다.

제어 컴포넌트

HTML에서 <input>, <textarea>, <select>와 같은 폼 엘리먼트는 일반적으로 사용자의 입력을 기반으로 자신의 state를 관리하고 업데이트하는데, React에서는 변경할 수 있는 state가 일반적으로 컴포넌트의 state 속성에 유지되고, setState()에 의해 업데이트됩니다.

React state를 “신뢰 가능한 단일 출처”로 만들면 두 요소를 결합할 수 있는데, 이렇게 두 요소를 결합하면 폼을 렌더링하는 React 컴포넌트는 폼에 발생하는 사용자 입력값을 제어할 수 있습니다.

제어 컴포넌트는 바로 이런 방식으로 React에 의해 값이 제어되는 입력 폼 엘리먼트를 말하는데, 다음의 코드와 같이 이전 예시가 전송될 때 이름을 기록하길 원하는 경우에 폼을 제어 컴포넌트로 작성할 수 있습니다.

제어 컴포넌트 예제
class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

위의 코드에서 value 어트리뷰트는 폼 엘리먼트에 설정되기 때문에 표시되는 값은 항상 this.state.value가 되고, React state신뢰 가능한 단일 출처가 되는데, 이 때 React state를 업데이트하기 위해 모든 키 입력에서 handleChange가 동작하기 때문에 사용자가 입력할 때 보여지는 값이 업데이트됩니다.

제어 컴포넌트를 사용하면, input의 값은 항상 React state에 의해 결정되는데, 이는 코드를 조금 더 작성해야 한다는 의미이지만, 대신 다른 UI 엘리먼트에 input의 값을 전달하거나 다른 이벤트 핸들러에서 값을 재설정할 수 있는 장점이 있습니다.

textarea

<textarea>
  Hello there, this is some text in a text area
</textarea>

HTML에서 <textarea> 엘리먼트는 텍스트를 자식으로 정의하지만, React에서는 <textarea>value 어트리뷰트를 대신 사용합니다. 이렇게하면 <textarea>를 사용하는 폼은 <input>과 같이 한 줄 입력을 사용하는 폼과 비슷하게 작성할 수 있습니다.

textarea 사용 예제
class EssayForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      value: 'Please write an essay about your favorite DOM element.'
    };

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('An essay was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Essay:
          <textarea value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

위 코드와 같이 React는 textarea의 value에 데이터를 바인딩하기 때문에 <input>과 비슷하게 사용하는 것을 확인할 수 있는데, this.state.value를 생성자에서 초기화하기 때문에 textarea는 일부 텍스트를 가진채 시작된다는 점에 주의할 필요가 있습니다.

select

<select>
  <option value="grapefruit">Grapefruit</option>
  <option value="lime">Lime</option>
  <option selected value="coconut">Coconut</option>
  <option value="mango">Mango</option>
</select>

HTML에서 <select>는 드롭 다운 목록을 만드는데, <select>selected라는 속성으로 Coconut이라는 옵션을 초기값으로 설정할 수 있습니다.

하지만 React에서는 selected 어트리뷰트를 사용하는 대신 최상단의 <select> 태그에서 value 어트리뷰트를 사용하는데, 이 방식은 한 곳에서 업데이트만 하면되기 때문에 제어 컴포넌트에서 사용하기에 더 편하다는 장점이 있습니다.

select 사용 예제
class FlavorForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: 'coconut'};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('Your favorite flavor is: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Pick your favorite flavor:
          <select value={this.state.value} onChange={this.handleChange}>
            <option value="grapefruit">Grapefruit</option>
            <option value="lime">Lime</option>
            <option value="coconut">Coconut</option>
            <option value="mango">Mango</option>
          </select>
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

코드와 같이 전반적으로 <input type="text">, <textarea>, <select>는 모두 매우 비슷하게 동작하는데, 이 요소들은 모두 제어 컴포넌트를 구현할 때 value 어트리뷰트를 허용합니다.

참고로 select 태그에 multiple 옵션을 허용하면, value 어트리뷰트에 배열을 전달할 수 있습니다.

select의 multiple 옵션
<select multiple={true} value={['B', 'C']}>

file input

<input type="file" />

HTML에서 <input type="file">는 사용자가 하나 이상의 파일을 자신의 로컬 저장소에서 서버로 업로드하거나, File API를 사용해 JavaScript로 조작할 수 있지만, <input type=”file”>는 값이 읽기 전용이기 때문에 React에서는 비제어 컴포넌트에 해당됩니다.

다중 입력 제어하기

다중 입력 제어는 여러 input 엘리먼트를 제어해야할 때, 각 엘리먼트에 name 어트리뷰트를 추가하고 event.target.name 값을 통해 핸들러가 어떤 작업을 할 지 선택할 수 있게 해줍니다.

다중 입력 제어 예제
class Reservation extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      isGoing: true,
      numberOfGuests: 2
    };

    this.handleInputChange = this.handleInputChange.bind(this);
  }

  handleInputChange(event) {
    const target = event.target;
    const value = target.type === 'checkbox' ? target.checked : target.value;
    const name = target.name;

    this.setState({
      [name]: value
    });
  }

  render() {
    return (
      <form>
        <label>
          Is going:
          <input
            name="isGoing"
            type="checkbox"
            checked={this.state.isGoing}
            onChange={this.handleInputChange} />
        </label>
        <br />
        <label>
          Number of guests:
          <input
            name="numberOfGuests"
            type="number"
            value={this.state.numberOfGuests}
            onChange={this.handleInputChange} />
        </label>
      </form>
    );
  }
}

코드는 주어진 input 태그의 name에 일치하는 state를 업데이트하기 위해 ES6의 computed property name 구문을 사용고 있습니다.

ES6 방식
this.setState({
  [name]: value
});

위 코드를 ES5 방식으로 표현하면 아래와 같은데, setState()는 자동적으로 현재 state에 일부 state를 병합하기 때문에 바뀐 부분에 대해서만 호출합니다.

ES5 방식
var partialState = {};
partialState[name] = value;
this.setState(partialState);

Null 값에 의한 오동작

제어 컴포넌트에 value prop을 지정하면 의도하지 않는 한 사용자가 변경할 수 없는데, value를 설정했는데도 여전히 수정할 수 있다면 이는 다음의 코드와 같이 실수로 valueundefinednull로 설정한 것일 수 있습니다.

ReactDOM.render(<input value="hi" />, mountNode);

setTimeout(function() {
  ReactDOM.render(<input value={null} />, mountNode);
}, 1000);

비제어 컴포넌트

React에서는 데이터를 변경할 수 있는 모든 방법에 대해 이벤트 핸들러를 작성하고, React 컴포넌트를 통해 모든 입력 상태를 연결해야 하기 때문에 제어 컴포넌트를 사용하는 것이 지루할 수도 있습니다.

특히 기존의 코드베이스를 React로 변경해야 하는 경우나, React가 아닌 다른 라이브러리와 React 애플리케이션을 통합하려는 경우에는 조금 짜증이 날 수도 있는데, 이런 경우에는 입력 폼을 구현하기 위한 대체 기술인 “비제어 컴포넌트”를 사용하는 것을 고려해 볼 수 있습니다.

대부분 경우에는 폼을 구현할 때 제어 컴포넌트를 사용하는 것이 좋은데, 제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어지는 반면, 비제어 컴포넌트는 DOM 자체에서 폼 데이터가 다루어집니다.

모든 state 업데이트에 대한 이벤트 핸들러를 작성하는 대신 비제어 컴포넌트를 만드는 경우에는 ref를 사용하여 DOM에서 폼 값을 가져올 수 있습니다.

class NameForm extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.input = React.createRef();
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.input.current.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

코드는 비제어 컴포넌트에 단일 이름을 허용하고 있는데, 비제어 컴포넌트는 DOM에 신뢰 가능한 출처를 유지하므로 비제어 컴포넌트를 사용할 때 React와 non-React 코드를 통합하는 것이 더 쉬울 수 있습니다.

비제어 컴포넌트는 빠르고 간편하게 적은 코드를 사용해 작성할 수 있지만, 일반적으로는 제어된 컴포넌트를 사용하는 것이 좋습니다.

기본 값

React 렌더링 생명주기에서 폼 엘리먼트의 value 어트리뷰트는 DOM의 value를 대체하는데, 비제어 컴포넌트를 사용하면 React 초깃값을 지정하지만, 그 이후의 업데이트는 제어하지 않는 것이 좋습니다.

이런 경우 value 어트리뷰트 대신 defaultValue를 지정할 수 있는데, 컴포넌트가 마운트된 후에 defaultValue 어트리뷰트를 변경해도 DOM의 값은 업데이트되지 않습니다.

참고로 <input type="checkbox"><input type="radio">defaultChecked를 지원하고, <select><textarea>defaultValue를 지원합니다.

render() {
  return (
    <form onSubmit={this.handleSubmit}>
      <label>
        Name:
        <input
          defaultValue="Bob"
          type="text"
          ref={this.input} />
      </label>
      <input type="submit" value="Submit" />
    </form>
  );
}

파일 입력 태그

<input type="file" />

HTML에서 <input type="file">은 사용자가 로컬 저장소에서 하나 이상의 파일을 선택하여 서버에 업로드하거나 파일 API를 사용하여 JavaScript로 조작할 수 있는데, React에서 <input type="file" />은 프로그래밍적으로 값을 설정 할 수 없고 사용자만이 값을 설정할 수 있기때문에 항상 비제어 컴포넌트로 동작합니다.

<input type=”file”>은 파일 API를 사용하여 파일과 상호작용해야 하는데, 다음의 예제와 같이 제출 핸들러에서 파일에 접근하기 위해 DOM 노드의 ref를 만드는 방법을 사용할 수 있습니다.

class FileInput extends React.Component {
  constructor(props) {
    super(props);
    this.handleSubmit = this.handleSubmit.bind(this);
    this.fileInput = React.createRef();
  }
  handleSubmit(event) {
    event.preventDefault();
    alert(
      `Selected file - ${this.fileInput.current.files[0].name}`
    );
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Upload file:
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">Submit</button>
      </form>
    );
  }
}

ReactDOM.render(
  <FileInput />,
  document.getElementById('root')
);

답글 남기기