[TIL] 220324

42770 단어 TILReactReact

📝 오늘 한 것

  1. react - 비제어 컴포넌트(ref 생성 및 접근) vs 제어 컴포넌트

📚 배운 것

새롭게 알게 된 내용이거나 이해하기 어려웠던 부분만 정리

1. 비제어 컴포넌트

React: 제어 컴포넌트와 비제어 컴포넌트의 차이점 참고

제어 컴포넌트에서 폼 데이터는 React 컴포넌트에서 다루어지고, 컴포넌트 내부 로직에 의해서 화면에 보여지는 값이 변한다.

반면에, 비제어 컴포넌트에서는 폼 데이터는 DOM 자체에서 다루어지고, 사용자에 의해 화면에 보여지는 값이 변한다.

1) Ref와 DOM

(1) ref란?

ref는 render 메서드에서 생성된 'DOM 노드'나 'React 엘리먼트'에 접근하기 위해 사용된다.
일반적으로 React의 데이터 플로우에서 부모 컴포넌트에서 자식 컴포넌트를 수정하려면 새로운 props를 전달하여 자식 컴포넌트를 다시 렌더링해야 한다.
그러나, ref를 사용하면 부모 컴포넌트에서 자식 컴포넌트를 직접 수정할 수 있다.

ref를 사용하지 않고 해결될 수 있다면 ref를 사용하는 것은 지양된다.
ref는 다음과 같은 상황에서 사용될 수 있다.

  • 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때
  • 애니메이션을 직접적으로 실행시킬 때
  • 서드 파티 DOM 라이브러리를 React와 같이 사용할 때

(2) ref 생성 및 접근

  • DOM 엘리먼트에 Ref 사용하기

    button input을 클릭하면 text input이 focus 되도록 하려고 한다.
    render 메서드에서 생성된 DOM 엘리먼트에 접근하려는 것이다.

    다음과 같이 ref 어트리뷰트가 HTML 엘리먼트에 쓰인 경우, 생성자에서 React.createRef()로 생성된 ref는, 자신을 전달받은 DOM 엘리먼트(다음 예제의 경우, input type="text")를 current 프로퍼티의 값으로 받는다.

class CumstomTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef(); // 컴포넌트 인스턴스가 생성될 때 ref를 그 프로퍼티로 추가
    this.focusTextInput = this.focusTextInput.bind(this);
  }
  
  focusTextInput() {
    this.textInput.current.focus();
    
    // console.log(this.textInput.current); // input
  }
  
  render() {
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} // 컴포넌트 인스턴스의 어느 곳에서도 ref에 접근 가능
        />
        <input
          type="button"
          value="focus"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}
  • 클래스 컴포넌트에 ref 사용하기

    한편, 여기서 더 나아가 button input을 직접 클릭하기도 전에 컴포넌트가 마운트된 직후 text input이 즉시 포커스 받도록 하려고 한다.
    render 메서드에서 React 컴포넌트의 인스턴스에 접근하려는 것이다.

    다음과 같이 ref 어트리뷰트가 커스텀 클래스 컴포넌트에 쓰인 경우, 생성자에서 React.createRef()로 생성된 ref는, 마운트된 컴포넌트의 인스턴스(다음 예제의 경우, CustomTextInput)를 current 프로퍼티의 값으로 받는다.

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  
  componentDidMount() {
    this.textInput.current.focusTextInput();
    
    // console.log(this.textInput.current); // CustomTextInput
  }
  
  render() {
    return (
      <CustomTextInput ref={this.textInput} />
    );
  }
}

ReactDOM.render(
  <AutoFocusTextInput />,
  document.getElementById("root")
);
  • 함수 컴포넌트에/에서 ref 사용하기

    (1) 반면에, 함수 컴포넌트는 인스턴스가 없기 때문에 함수 컴포넌트'에는' ref 어트리뷰트를 사용할 수 없다.

    (2) 그러나, 함수 컴포넌트'에서' (render 메서드에서 생성된 DOM 엘리먼트에 접근하기 위해) ref 어트리뷰트를 사용하는 것은 가능하다.

    cf. 이때 함수 컴포넌트에서는 createRef 대신 useRef를 사용해야 한다.
    함수 컴포넌트에서 createRef를 사용하면 setState가 호출되고 컴포넌트가 리렌더링될 때마다 createRef도 다시 호출되어 ref.current가 null로 초기화되기 때문이다.
    따라서, ref.current의 값을 유지하기 위해 useRef를 사용해야 한다.

// (1)의 경우
function MyFunctionComponent() {
  return <input />;
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }
  render() {
    // 함수 컴포넌트'에는' ref 어트리뷰트 사용 불가능
    return (
      <MyFunctionComponent ref={this.textInput} />
    );
  }
}
// (2)의 경우
function CustomTextInput(props) {
  // textInput은 ref 어트리뷰트를 통해 전달되기 위해서 이곳에서 정의되어야 함
  const textInput = useRef(null);

  function handleClick() {
    textInput.current.focus();
  }

  return (
    <div>
      <input
        type="text"
        ref={textInput} /> {/* 함수 컴포넌트'에서' ref 어트리뷰트 사용 가능 */}
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

부모 컴포넌트에서, '자식 컴포넌트의 DOM 엘리먼트'에 접근하고자 할 때 ref를 사용할 수 있다.
(위 예제들은 '직접 렌더링하는 DOM 엘리먼트' 혹은 부모 컴포넌트에서 '자식 컴포넌트'에 접근하기 위해 ref를 사용한 경우였다.)

'자식 컴포넌트의 DOM 엘리먼트'에 접근하기 위해서도 위와 같이 자식 컴포넌트에 ref 어트리뷰트를 지정하는 방식을 사용할 수 있긴 하지만
이는 위에서 살펴봤듯 자식 컴포넌트의 인스턴스의 DOM 엘리먼트가 아니라 자식 컴포넌트의 인스턴스를 가져온다는 점에서, 그리고 자식 컴포넌트가 함수 컴포넌트인 경우에는 자식 컴포넌트에 사용할 수 없는다는 점에서, 좋은 방법이 아니다.

대신에 (React 16.3 이후 버전의 React를 사용 중이라면) 부모 컴포넌트에서 '자식 컴포넌트의 DOM 엘리먼트'에 접근하기 위해 ref를 전달하는 방법을 사용할 수 있다.
이는 부모 컴포넌트가 자식 컴포넌트의 ref를 자신의 ref로서 외부에 노출시키는 방법을 말한다.

React.forwardRef()를 이용해 일부 컴포넌트(다음 예제의 경우, < FancyButton />)가 수신한 ref를 받아 조금 더 아래로 전달(즉, '전송')할 수 있는 옵트인 기능이다. (옵트인: 당사자가 개인 데이터 수집을 허용하기 전까지 당사자의 데이터 수집을 금지하는 제도)
ref 어트리뷰트가 HTML 엘리먼트에 쓰인 경우, ref 객체는 자신을 전달받은 DOM 엘리먼트를, 그 current 프로퍼티의 값으로 받는 것을 이용한 방법이다.

// 3. 이 ref를 forwardRef 내부의 (props, ref) => ... 함수의 두 번째 인자로 전달
const FancyButton = React.forwardRef((props, ref) => {
  return (
    <button ref={ref} className="FancyButton"> {/* 4. 이 ref를 <button>으로 전달 */}
      {props.children}
    </button>
  );
});

const App = () => {
  const ref = React.createRef(); // 1. createRef를 호출하여 ref를 생성하고 ref 변수에 할당
  
  const onMouseOver = () => {
    ref.current.focus();
    
    // 5. 최초 컴포넌트 렌더링이 끝나면, ref.current는 <button> DOM 노드를 가리키게 됨
    // console.log(ref.current); // <button class="FancyButton">Click me!</button>
  };
  
  return (
    <div>
      <input onMouseOver={onMouseOver}/>
      <FancyButton ref={ref}>Click me!</FancyButton> {/* 2. ref를 <FancyButton />으로 전달 */}
    </div>
  );
};

ReactDOM.render(
  <App/>,
  document.getElementById("root")
);

(3) 콜백 ref

ref를 설정하기 위한 또 다른 방법으로 ref가 설정되고 해제되는 상황을 세세하게 다룰 수 있는 콜백 ref가 있다.

위에서 살펴봤던 것과는 달리 ref 어트리뷰트에 React.createRef()를 통해 생성된 ref 객체를 전달하는 대신에, ref 콜백 함수를 전달한다.

ref 콜백은 인라인 함수로 선언하기보다 클래스에 바인딩된 메서드로 선언하는 것이 좋다.

한편, ref를 수정하는 작업과 ref 콜백 모두 componentDidMount 또는 componentDidUpdate가 호출되기 전에 수행되고 호출된다.

  • DOM 엘리먼트에 콜백 ref 전달하기
class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);

    this.textInput = null; // 1. DOM 엘리먼트(또는 자식 컴포넌트 인스턴스)에 대한 참조를 저장할, 부모 컴포넌트 인스턴스의 프로퍼티 만들기

    this.setTextInputRef = element => {
      this.textInput = element; // 4. 이를 부모 컴포넌트 인스턴스 프로퍼티에 저장함
      
      // 3.  ref 콜백은 ref 어트리뷰트가 쓰인 DOM 엘리먼트(또는 자식 컴포넌트의 인스턴스)를 인자로 받음
      // console.log(element); // <input type="text">
    };

    // ( constructor 안에 정의되어 있으므로 밑에서 이벤트 핸들러 함수로 사용될 때 this 바인딩 안해도 된다 )
    this.focusTextInput = () => {
      // 6. 최초 컴포넌트 렌더링 시 자동으로 포커스 됨, 그 후 버튼 누를 때마다 포커스됨
      // => render 메서드에 의해 생성되는 DOM 엘리먼트에 접근할 수 있게 되었다!
      if (this.textInput) {
        this.textInput.focus();
      }
    };
  } // ( ...여기까지가 constructor )

  componentDidMount() { // 5. 최초 컴포넌트 렌더링이 끝난 후 componentDidMount 메서드가 호출됨
    this.focusTextInput();
  }

  render() {
    return (
      <div>
        <input
          type="text"
          ref={this.setTextInputRef} {/* 2. render 메서드가 호출되면 componentDidMount 호출 전에 ref 콜백이 호출됨 */}
        />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

ReactDOM.render(
  <CustomTextInput/>,
  document.getElementById("root")
);
  • 자식 컴포넌트에 콜백 ref 전달하기
function CustomTextInput(props) {
  return (
    <div>
      <input ref={props.inputRef} /> {/* 함수 컴포넌트'에선' ref를 사용할 수 있다. */}
    </div>
  );
}

class Parent extends React.Component {
  constructor(props) {
    super(props);
    
    this.inputElement = null;
  }
  
  inputRef(element) {
    this.inputElement = element;
  }
  
  componentDidMount() {
    this.inputElement.focus();
  }
  
  render() {
    return (
      <CustomTextInput inputRef={this.inputRef.bind(this)} />
      {/* props 프로퍼티로 inputRef(ref 콜백)를 전달했다.
      ※ 주의! 함수 컴포넌트'에는' ref는 사용할 수 없다.
      여기서는 ref를 사용한 게 아니라 inputRef라는 prop을 작성한 것이다.  */}
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById("root")
);

2) defaultValue

React 렌더링 생명 주기에서 폼 엘리먼트의 value 어트리뷰트는 DOM의 value를 대체한다.
비제어 컴포넌트를 사용할 때 초깃값을 지정해줄 순 있지만, 그 이후의 업데이트는 제어하지 않는 것이 좋다.
이를 위해 JSX에서의 value 어트리뷰트 대신 defaultValue 어트리뷰트를 사용할 수 있다.
컴포넌트가 마운트 된 후에 defaultValue 어트리뷰트를 변경해도 DOM의 값은 업데이트 되지 않는다.

< input type="checkbox" >와 < input type="radio" >는 defaultChecked를 지원하고, < input >과 < select >, < textarea >는 defaultValue를 지원한다.

class CustomForm extends React.Component {
  constructor(props) {
    super(props);
    
    this.input = React.createRef();
 
    this.handleSubmit = this.handleSubmit.bind(this);
  }
  
  handleSubmit(event) {
    event.preventDefault();
    console.log(this.input.current.value);
  }
  
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input
            defaultValue="Bob"
            type="text"
            ref={this.input} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

ReactDOM.render(
  <CustomForm />,
  document.getElementById("root")
);

3) 파일 입력 태그

React에서 <input type="file" />은 프로그래밍적으로 값을 설정 할 수 없고 사용자만이 값을 설정할 수 있기 때문에 항상 비제어 컴포넌트입니다.

다음 예제에서는 onSubmit 이벤트 핸들러 함수에서 파일에 접근하기 위해서 DOM 엘리먼트에 ref를 만들었다.

const CustomForm = () => {
  const fileInput = React.useRef(null);
  
  const onSubmit = (event) => {
    event.preventDefault();
    console.log(fileInput.current.files[0].name);
  };
  
  return (
    <div>
      <form onSubmit={onSubmit}>
        <label htmlFor="file">
          파일: 
          <input id="file" type="file" ref={fileInput} />
        </label>
        <input type="submit" value="submit" />
      </form>
    </div>
  );
};

ReactDOM.render(
  <CustomForm />,
  document.getElementById("root")
);

✨ 내일 할 것

  1. react effects

좋은 웹페이지 즐겨찾기