들어가기 전에

리액트 공식문서로 배워보자 #9, 폼

HTML form 엘리먼트는 리액트의 다른 DOM 엘리먼트와는 약간 다르게 동작합니다. 왜냐하면 form 엘리먼트는 본래 어떤 내부 정보를 갖고 있기 때문입니다. 예를 들면, 이 순수한 HTML 속 form은 하나의 이름을 받습니다.

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

이 폼은 사용자가 submit하였을 때, 새로운 페이지로 브라우징하는 기본 HTML 폼의 행위를 기본 값으로 가지고 있습니다. 만일 리액트에서 폼이 이렇게 작동하길 원하면, 그냥 작성하시면 됩니다. 하지만 대부분의 경우에, 폼의 submit과 유저가 폼에 입력한 데이터에 대한 접근권한을 다루는 자바스크립트 함수를 갖는 것이 편리합니다. 이러한 기능을 달성하기 위한 표준적인 방법은 "제어된(controlled) 컴포넌트"라고 불리는 기술을 이용합니다.

제어된(controlled) 컴포넌트

HTML에서, <input>, <textarea>, <select> 와 같은 form 엘리먼트는 일반적으로 유저의 입력값을 기반으로 그들 자체의 상태를 관리하고 업데이트합니다. 리액트에서, 변할 수 있는 상태는 일반적으로 컴포넌트의 상태 프로퍼티 내부에 보관됩니다. 그리고 오직 setState() 메소드를 통해서 업데이트됩니다.

우리는 리액트의 상태를 "single source of truth"로 만듦으로써 두개를 합칠 수 있습니다. 그렇게 하면, form을 렌더링하는 리액트 컴포넌트는 폼 내부에서 유저가 무엇을 입력하는지에 따라 어떤 일이 일어날지 제어합니다. 이러한 방식으로, 리액트에 의해 값이 컨트롤 되는 엘리먼트를 가진 입력 폼을 "제어된(controlled) 컴포넌트"라 부릅니다.

예를 들어, 만일 여러분이 submit되었을 때, 이전 예제가 name을 로깅하길 원한다면, 우리는 다음과 같은 제어된 컴포넌트를 작성할 수 있습니다.

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 속성이 우리 form 엘리먼트에서 세팅되었고, React state를 source of truth로 만들었기 때문에, 표시되는 값은 언제나 this.state.value일 것입니다. 키를 누를 때마다, handleChange가 리액트 상태를 업데이트시키기 위해 동작하기 때문에, 유저가 무언가를 타이핑할 때마다 표시되는 값이 업데이트됩니다.

제어된 컴포넌트와 함께, 모든 상태 변화는 관련된 핸들러 함수를 갖을 것입니다. 이러한 점은 유저 입력 값의 수정이나 검증을 더욱 직관적으로 만들어줍니다. 예를 들어, 우리가 만일 어떤 이름을 강조하고 싶어서 모든 글자를 대문자로 쓰고 싶다면, 우리는 handleChange를 다음과 같이 바꾸면 됩니다.

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

textarea 태그

HTML에서, <textarea> 엘리먼트는 자식에 의해 텍스트를 정의합니다.

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

리액트에서, <textarea>value 속성을 대신 사용합니다. <textarea>를 사용하는 방식은 단 한줄의 입력을 받는 form과 매우 비슷하게 작성될 수 있습니다.

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>
    );
  }
}

this.state.value가 생성자에서 초기화 되는 것을 잘 보세요. textarea가 어떤 텍스트를 가지면서 생성됩니다.

select 태그

HTML에서, <select> 는 드롭 다운 리스트를 만듭니다. 예를 들면, 이 HTML은 맛(flavors)의 드롭다운 리스트를 만듭니다.

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

selected속성에 의해 코코넛 옵션이 초기 값으로 선택되어 있다는 것을 잘 보세요. 리액트는 selected 속성을 사용하는 대신에, value 속성을 root의 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>이 다 비슷하게 작동합니다. 이 3가지 엘리먼트 모두 제어된 컴포넌트를 구현하기 위한 value 속성을 받습니다.

알아둬야 할 것

여러분은 array를 value 속성에 넣을 수 있습니다. select 태그에서 여러 개의 옵션을 선택하는 것을 허용합니다.

파일 입력 태그

HTML에서, <input type="file">은 디바이스 저장소로부터 서버에 업로드하기 위해 또는 자바스크립트로 다루기 위해 File API를 이용해 사용자에게 한개 또는 여러개의 파일을 선택하게 해주었습니다.

<input type="file" />

리액트에서는 제어되지 않는(uncontrolled) 컴포넌트 입니다. 왜냐하면 이 것이 갖는 값은 읽기-전용이기 때문입니다. 이건 나중 문서에서 다른 제어되지 않는 컴포넌트와 함께 다뤄볼 것입니다.

여러개의 입력값 다루기

여러 개의 제어된 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>
    );
  }
}

코드펜에서 직접 해보기

우리가 입력받은 이름에 대한 상태 키를 업데이트 하기 위해 ES6 문법인 computed property name 문법을 사용했다는 것을 알아두세요.

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

ES5 문법으로 치면 다음과 같습니다.

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

또한 setState() 메소드가 자동으로 부분 상태를 현재의 상태로 통합했기 때문에, 우리는 변화된 부분만 호출할 필요가 있었습니다.

제어된(controlled) 입력 Null 값

제어된 컴포넌트위의 value prop을 지정하는 것은 사용자 입력을 원치 않을 때, 사용자가 입력 값을 변경하는 것을 막아줍니다. 만일 여러분이 value를 지정했지만 입력 값을 여전히 편집할 수 있다면, 여러분은 실수로 valueundefined 또는 null로 설정할 수 있습니다.

다음 코드는 위에서 설명한 것을 보여줍니다. (입력 값은 처음엔 잠기지만 짧은 딜레이 이후에는 편집 가능하게 변합니다.)

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

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

제어된 컴포넌트의 대체품(Alternatives)

제어된 컴포넌트를 사용하는 것은 때때로 지루할 수 있습니다. 왜냐하면 데이터가 변화하는 것에 대해 이벤트 핸들러를 매번 작성해야 할 필요가 있고 모든 입력 상태를 리액트 컴포넌트를 통해 주고 받아야 하기 때문입니다. 이미 존재하던 코드베이스를 리액트로 전환하거나 리액트 어플리케이션과 리액트 어플리케이션이 아닌 것을 통합할 때, 이러한 일은 꽤나 짜증나는 일이 될 수 있습니다. 이러한 일이 있을 때, 여러분은 입력 폼을 구현하기 위한 대체 기술인 제어되지 않은 컴포넌트를 체크하길 원할겁니다.

완전한 해결책

만일 당신이 검증, 방문했던 필드 추적, 그리고 폼 전송을 포함한 완벽한 솔루션을 찾고 있다면, Formik은 유명하고 사람들이 많이 선택하는 솔루션입니다. 하지만, 이것도 제어된 컴포넌트와 상태관리 같은 원칙에 따라 만들어진 것입니다. 그러니 이러한 것들을 배우는 것을 무시하진 마세요.