Form Controls

React에 의해 제어되는 폼 컨트롤

HTML 폼 컨트롤 방식

HTML 폼 컨트롤 요소(<input />, <textarea>, <select> 등)는 사용자의 입력 내용을 자체적으로 관리합니다. 각 컨트롤의 value 값을 JavaScript를 사용해 확인해 입력 내용을 읽거나, 쓸 수 있습니다.

const inputEmail = document.querySelector('form input[type="email"]')

// GET: HTML 폼 컨트롤 속성 값 읽기
console.log(inputEmail.value)

// SET: HTML 폼 컨트롤 속성 값 쓰기
inputEmail.value = 'yamoo9@euid.dev'

React 폼 컨트롤 방식

클래스 컴포넌트 상태 정보를 state 속성으로 관리하며 setState() 메서드를 사용해 상태를 업데이트 합니다.

class FormControl extends React.Component {

  state = {
    value: '',
  }

  handleChange = (e) => {
    this.setState({ value: e.target.value })
  }

  render() {
    return (
      <input
        value={this.state.value}
        onChange={this.handleChange}
      />
    )
  }
  
}

라이브 예제

Controlled 컴포넌트

React 컴포넌트는 폼을 통해 입력된 사용자의 값을 제어 할 수 있습니다. React를 통해 값이 관리되는 입력 요소는 컨트롤 컴포넌트(Controlled Component)입니다. 예를 들면 다음의 HTML 폼(<input /> 포함)을 React에 의해 컨트롤 되는 컴포넌트로 변경할 수 있습니다.

<form onsubmit="handleSubmit()">
  <label>
    @이메일: 
    <input type="email" name="email" placeholder="yamoo9@euid.dev" />
  </label>
  <button type="submit">전송</button>
</form>
class Form extends React.Component {

  state = {
    email: ''
  }
  
  handleInput = (e) => {
    this.setState({ email: e.target.value })
  }
  
  handleSubmit = (e) => {
    e.preventDefault()
    console.log(`입력하신 이메일 ${this.state.email}이 성공적으로 입력되었습니다.`)
  }
  
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          @이메일:
          <input
            type="email"
            name="email"
            value={this.state.email}
            onChange={e => this.handleInput(e)} 
          />
        </label>
        <button type="submit">전송</button>
      </form>
    )
  }
  
}

컨트롤 컴포넌트를 사용하면 모든 state의 업데이트는 연결 된 이벤트 리스너에 의해 처리됩니다. 컨트롤 컴포넌트를 통해 사용자의 입력 내용을 업데이트 하거나, 유효성 검사 수행이 가능합니다. 예를 들어 입력 내용의 앞/뒤에 공백을 제거하고 싶다면 리스너 코드를 다음과 같이 수정합니다.

handleInput = (e) => {
  this.setState({
    email: e.target.value.trim()
  })
}

멀티 이벤트 핸들링

하나 이상의 <input />을 하나의 리스너로 컨트롤 할 수 있습니다. 여러 개의 컨트롤을 제어하기 위해서는 name 속성을 설정해야 합니다. 사용자가 입력한 등록 정보(email, password)를 처리하는 이벤트 리스너 예를 살펴봅니다.

class MultiControlInputs extends React.Component {

  state = {
    // 사용자 등록 정보 객체 (이메일, 패스워드)
    user: {
      email: '',
      password: ''
    }
  }

  // 멀티 이벤트 핸들링 리스너
  handleChange = (e) => {
    // 이벤트 타겟 객체 구조 분해 할당
    const {name, value} = e.target
    // user 상태 업데이트
    this.setState({
      user: {
        ...this.state.user,
        [name]: value 
      }
    })
  }

  render() {
    const { user: { email, password } } = this.state
    
    return (
      <>
        <input
          type="email"
          name="email"
          aria-label="계정 이메일"
          value={email}
          onChange={this.handleChange} />
        <input
          type="password"
          name="password"
          aria-label="계정 패스워드"
          value={password}
          onChange={this.handleChange} />
      </>
    )
  }
  
}

라이브 예제

TextArea 컨트롤

HTML <textarea> 요소는 사용자가 입력한 내용을 자식 텍스트 콘텐츠로 받습니다.

<textarea>오늘 하루를 정리하는 글을 작성해봅시다.</textarea>

하지만 React는 value 속성을 대신 사용합니다. 즉, 한 줄 입력을 사용하는 폼 컨트롤과 비슷하게 작성합니다.

<textarea
  value={this.state.value}
  onChange={this.handleChange}
/>

라이브 예제

Select 컨트롤

HTML <select> 요소는 드롭 다운 메뉴를 화면에 렌더링 하고, 초기 값을 selected 속성을 사용해 처리합니다.

<select class="learning-subject">
  <option value="">학습 할 주제를 선택하세요.</option>
  <option value="react" selected>리엑트</option>
  <option value="reactRouter">리엑트 라우터</option>
  <option value="redux">리덕스</option>
</select>

반면, React는 value 속성을 사용해 초기 값을 설정합니다.

<select
  className="learning-subject"
  value={this.state.value}
  onChange={this.handleChange}
>
  <option value="">학습 할 주제를 선택하세요.</option>
  <option value="react" selected>리엑트</option>
  <option value="reactRouter">리엑트 라우터</option>
  <option value="redux">리덕스</option>
</select>

라이브 예제

Multiple Select 컨트롤

HTML <select> 요소를 사용해 하나 이상의 아이템을 선택할 수 있는 멀티플 드롭 다운 메뉴를 사용하려면 multiple 속성을 요소에 추가합니다.

<select class="learning-subject" multiple>
  <option value="">학습 할 주제를 선택하세요.</option>
  <option value="react" selected>리엑트</option>
  <option value="reactRouter">리엑트 라우터</option>
  <option value="redux">리덕스</option>
</select>

반면 React에서는 <select> 요소에 다음의 2가지 설정이 요구 됩니다.

  1. multiple 속성 값은 true 불리언(Boolean) 데이터 유형으로 설정

  2. value 속성 값은 배열(Array) 데이터 유형으로 설정

<select
  className="learning-subject"
  multiple={true}
  value={this.state.value}
  onChange={this.handleChange}
>
  <option value="">학습 할 주제를 선택하세요.</option>
  <option value="react" selected>리엑트</option>
  <option value="reactRouter">리엑트 라우터</option>
  <option value="redux">리덕스</option>
</select>

value 초기 값을 배열로 설정한 후, 핸들러 내부에서 사용자의 멀티 선택을 배열 값으로 업데이트 해야 합니다.

state = {
  value: []
}

handleChange(e) {
  // select > option 요소 수집 후 배열 데이터로 변경
  const options = Array.from(e.target.children)
  // 사용자가 선택한 option 필터링
  const selectedOptions = options.filter(option => option.selected)
  // 필터링 된 option.value 값을 아이템으로 하는 새로운 배열 반환
  const selectedOptionsValue = selectedOptions.map(option => option.value)
  // 상태 업데이트
  this.setState({value: selectedOptionsValue});
}

라이브 예제

Uncontrolled 컴포넌트

컨트롤 되지 않은(Uncontrolled) 컴포넌트는 React 외부에서 작동되는 것처럼 처리됩니다. 사용자가 폼 입력 컨트롤 (input, dropdown 등)에 입력하면 업데이트 된 정보가 React에서 별도 처리 과정 없이 요소에 바로 반영됩니다. 즉, React에 의해 "컨트롤 되지 않음"을 의미 합니다.

특별한 경우를 제외하고, 폼 컨트롤은 React에서 제어(Controlled) 하는 것이 권장됩니다.

File 인풋 요소

HTML <input type="file" /> 요소는 하나 이상의 파일을 사용자의 로컬 컴퓨터에서 서버로 업로드 할 때 사용합니다.

<input type="file" />

이 컴포넌트는 프로그래밍 방식으로 값을 설정 할 수 없고, 사용자에 의해 값이 설정되는 특별한 경우로 "컨트롤 할 수 없는 컴포넌트" 입니다. 컨트롤 할 수 없는 컴포넌트는 React가 아닌, DOM 자체에서 데이터를 다뤄야 합니다. (File API 사용)

모든 state 업데이트를 처리하는 이벤트 핸들러 대신, "컨트롤 할 수 없는 컴포넌트"를 제어하기 위해 직접 DOM을 통해 요소의 사용자 입력 값을 가져와 처리해야 합니다. 문제는 "가상 DOM에서 어떻게 실제 DOM에 접근할 것인가?" 입니다.

DOM 노드 참조

ref 속성은 컴포넌트가 렌더링 된 이후의 실제 DOM 노드나 React 요소에 접근해야 할 때 사용하는 방법입니다. 아래 예시는 핸들러에서 파일에 접근하기 위해 DOM 노드의 Ref(참조)를 만드는 방법을 보여주고 있습니다.

참조(Ref.) 생성

ref 속성을 활용하려면 먼저 React.createRef() 메서드를 사용해 참조 객체를 생성해야 합니다. 생성 된 참조(Ref.)는 ref 속성을 통해 DOM 노드 또는 React 요소를 가리키게 됩니다.

class FileInput extends Component {
  
  // 참조 객체 생성
  fileInput = React.createRef(null);

  handleSubmit = (event) => {
    event.preventDefault();
    console.log(`선택된 파일: ${this.fileInput.current.files[0].name}`)
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          업로드:
          {/* 참조 할 노드의 ref 속성에 참조 객체 연결 */}
          <input type="file" ref={this.fileInput} />
        </label>
        <br />
        <button type="submit">전송</button>
      </form>
    );
  }

}

참조(Ref.) 접근

ref 속성이 설정 된 DOM 요소는 ref.current 속성을 통해 접근할 수 있습니다.

handleSubmit = (event) => {
  event.preventDefault();
  console.log(`선택된 파일: ${this.fileInput.current.files[0].name}`)
}

React v16.8 부터는 useRef() 훅을 사용해 함수형 컴포넌트에서도 ref 속성을 사용할 수 있습니다.

import React, {useRef} from 'react'


const FunctionalComponent = (props) => {
  const divRef = useRef(null)
  
  return (
    <div ref={divRef}> ... </div>
  )
}

이벤트 대상 활용

사용자에 의해 실행되는 이벤트 리스너를 사용해 핸들링 하는 경우, 반드시 참조(Ref.)를 통해 접근해야 하는 것은 아닙니다. 이벤트 객체를 통해 대상(event.target) 요소에 접근해 조작할 수도 있습니다.

라이브 예제

Last updated