TIL 55 | Ref 이용하여 핀터레스트 스타일 레이아웃 (masonry) 만들기

hyounglee·2020년 9월 17일
1

React

목록 보기
14/33
post-thumbnail

참고자료 : React에서 Ref 사용하기, Ref와 DOM(공식 문서)

리액트는 컴포넌트 트리 선언과 props 사용을 통해서, DOM 노드에 레퍼런스를 걸지 않고도 UI 제어가 대부분 가능하다. props는 부모 컴포넌트가 자식과 상호작용할 수 있는 유일한 수단이다. 자식을 수정하기 위해서는 새로운 props를 전달하여 자식을 다시 렌더링 해야 한다.

하지만, 일반적인 데이터 플로우에서 벗어나 직접적으로 자식을 수정해야 하는 경우도 생기는데, 수정할 자식은 React 컴포넌트의 인스턴스일 수도 있고, DOM 엘리먼트일 수도 있다.

React Ref는 특정한 DOM 혹은 컴포넌트의 인스턴스에 reference를 걸어준다.** 이 Ref를 통해서 render 메서드에서 만든 DOM 노드나 React 요소에 접근해서, 값을 얻거나 수정할 수 있다.**

Ref 어떻게 쓸까?

Ref 생성하기

Ref는 React.createRef()를 통해 생성되고, ref attribute를 통해 React 엘리먼트에 부착된다. 보통, 컴포넌트의 인스턴스가 생성될 때 Ref를 프로퍼티로서 추가하고, 그럼으로서 컴포넌트의 인스턴스의 어느 곳에서도 Ref에 접근할 수 있게 된다.

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.myRef = React.createRef();
  }
  render() {
    return <div ref={this.myRef} />;
  }
}

Ref에 접근하기

render 메서드 안에서 ref가 엘리먼트에게 전달되었을 때, 그 노드를 향한 참조는 ref의 current 어트리뷰트에 담기게 된다.

const node = this.myRef.current;

ref의 값은 노드의 유형에 따라 다르다.

  • DOM 엘리먼트
    ref 어트리뷰트가 HTML 엘리먼트에 쓰였다면, 생성자에서 React.createRef()로 생성된 ref는 자신을 전달받은 DOM 엘리먼트를 current 프로퍼티의 값으로서 받는다.

  • 클래스 컴포넌트
    ref 어트리뷰트가 커스텀 클래스의 컴포넌트에 쓰였다면, ref 객체는 마운트된 컴포넌트의 인스턴스를 current 프로퍼티의 값으로서 받는다.

  • 함수 컴포넌트는 인스턴스가 없기 때문에 함수 컴포넌트에 ref 어트리뷰트를 사용할 수 없다.

DOM 엘리먼트에 Ref 사용하기

아래의 코드는 DOM 노드에 대한 참조를 저장하기 위해 ref 를 사용한다.

class CustomTextInput extends React.Component {
  constructor(props) {
    super(props);
    // textInput DOM 엘리먼트를 저장하기 위한 ref를 생성합니다.
    this.textInput = React.createRef();
    this.focusTextInput = this.focusTextInput.bind(this);
  }

  focusTextInput() {
    // DOM API를 사용하여 명시적으로 text 타입의 input 엘리먼트를 포커스합니다.
    // 주의: 우리는 지금 DOM 노드를 얻기 위해 "current" 프로퍼티에 접근하고 있습니다.
    this.textInput.current.focus();
  }

  render() {
    // React에게 우리가 text 타입의 input 엘리먼트를
    // 우리가 생성자에서 생성한 `textInput` ref와 연결하고 싶다고 이야기합니다.
    return (
      <div>
        <input
          type="text"
          ref={this.textInput} />
        <input
          type="button"
          value="Focus the text input"
          onClick={this.focusTextInput}
        />
      </div>
    );
  }
}

컴포넌트가 마운트 될 때 React는 current 프로퍼티에 DOM 엘리먼트를 대입하고, 컴포넌트의 마운트가 해제될 때 current 프로퍼티를 다시 null로 돌려놓는다. ref를 수정하는 작업은 componentDidMount 또는 componentDidUpdate 생명주기 메서드가 호출되기 전에 이루어진다.

클래스 컴포넌트에 ref 사용하기

아래에 있는 CustomTextInput 컴포넌트 인스턴트가 마운트 된 이후에 즉시 클릭되는 걸 흉내내기 위해 CustomTextInput 컴포넌트를 감싸는 것을 원한다면, ref를 사용하여 CustomTextInput 컴포넌트의 인스턴스에 접근하고 직접 focusTextInput 메서드를 호출할 수 있다.

class AutoFocusTextInput extends React.Component {
  constructor(props) {
    super(props);
    this.textInput = React.createRef();
  }

  componentDidMount() {
    this.textInput.current.focusTextInput();
  }

  render() {
    return (
      <CustomTextInput ref={this.textInput} />
    );
  }
}

위 코드는 CustomTextInput이 클래스 컴포넌트일 때만 작동한다! 함수 컴포넌트에서 ref를 사용할 수 있도록 하려면, forwardRef (높은 확률로 useImperativeHandle와 함께)를 사용하거나 클래스 컴포넌트로 변경할 수 있다.

다만, DOM 엘리먼트나 클래스 컴포넌트의 인스턴스에 접근하기 위해 ref 어트리뷰트를 함수 컴포넌트에서 사용하는 것은 가능하다!

Ref 활용하기!

핀터레스트 형식의 레이아웃 만들기

카카오 프렌즈샵의 홈-메인탭 레이아웃은 핀터레스트 형식처럼 다양한 크기의 카드가 동일한 마진의 간격을 두고 이어지는 조적조(벽돌) 형식을 띄고 있다. 각 카드 컴포넌트는 렌더가 되기 전까지는 고정된 height 값을 갖지 않기 때문에, 렌더가 될 때 각 컴포넌트의 height 값을 알아낼 수 있어야 했다.

구글링해서 찾았던 DOM 활용 방식

  componentDidMount() {
    window.onload = () => {
      let images = document.querySelectorAll(".HomeCard");
      console.log(images.length);
      for (let i = 0; i < images.length; i++) {
        console.log(i, images[i].children[0].clientHeight);
        images[i].style.gridRowEnd = `span ${Math.floor(
          images[i].children[0].clientHeight / 10
        )}`;
      }
    };
  }

(수정된) ref 활용 방식

React에서 DOM에 직접 접근하여 값을 업데이트 하는 것은 지양해야 한다. DOM에서 클래스명으로 찾아 반복문으로 변하는 값을 부여하는 로직을 ref로 만들어보자.

class HomeCard extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      height: 0,
    };
    this.divElement = React.createRef();
  }
  
  componentDidMount() {
    const height = this.divElement.current.clientHeight;
    this.setState({ height });
  }
  
  render() {
    const { height } = this.state;
    return (
      <div
        className="HomeCard"
        style={{
          gridRowEnd: `span ${Math.floor(height / 10)}`,
        }}
        ref={this.divElement}
      >

아래와 같은 방식으로도 가능하다고 한다!

class HomeCard extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      height: 0,
    };
    // this.divElement = React.createRef();
  }

  componentDidMount() {
    this.setState({
      height: this.divElement.clientHeight,
    });
    // const height = this.divElement.current.clientHeight;
    // this.setState({ height });
  }

  render() {
    const { card } = this.props;
    const { height } = this.state;
    return (
      <div
        className="HomeCard"
        style={{
          gridRowEnd: `span ${Math.floor(height / 10)}`,
        }}
        ref={(ref) => (this.divElement = ref)}
      >
profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

0개의 댓글