[React]Ref

OnStar·2021년 9월 2일
0

React

목록 보기
10/11
post-thumbnail

Ref는 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근하는 방법을 제공합니다. - 리액트 공식문서

Ref 의 탄생배경

일반적인 React의 데이터 흐름에서 props는 부모 컴포넌트가 자식과 상호작용 할 수 있는 유일한 수단이다. 자식 컴포넌트를 수정하려면 새로운 props를 전달하여 자식을 다시 렌더링해야한다. 그러나 일반적인 데이터 플로우를 벗어나 자식 컴포넌트를 수정해야 하는 경우도 있다. React 컴포넌트의 인스턴스, DOM 엘리먼트의 수정에 대한 해결책이다.

createRef? useRef?

React 에서는 ref 기능을 이용하기 위해 createRef()와 useRef()를 제공한다. 이 둘을 간략하게 구분하면 클래스형 컴포넌트에서는 createRef를, 함수형 컴포넌트에서는 useRef()를 사용한다.

// 클래스형 컴포넌트의 createRef()
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가 클래스 컴포넌트일 때에만 작동한다는 점을 기억
class CustomTextInput extends React.Component {
  // ...
}

// 함수형 컴포넌트의 useRef()
function CustomTextInput(props) {
  // textInput은 ref 어트리뷰트를 통해 전달되기 위해서
  // 이곳에서 정의되어야만 한다.
  const textInput = useRef();

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

  return (
    <div>
      <input
        type="text"
        ref={textInput} />
      <input
        type="button"
        value="Focus the text input"
        onClick={handleClick}
      />
    </div>
  );
}

  • 클래스형 컴포넌트는 인스턴스를 생성해 render 메소드를 호출하는 방식으로 리렌더링을 한다.
  • 함수형 컴포넌트는 해당 함수를 리렌더링 마다 다시 실행한다.

따라서 함수형 컴포넌트에서 createRef() 사용하면 컴포넌트의 렌더링마다 ref가 생성된다.

Mutable(가변성)

중요한 것은 createRef()와 useRef() 자체는 변경가능한 객체를 만드는 기능이라는 점이다.

ref는 componentDidMount나 useEffect가 실행되기 전에 할당된다. 즉,컴포넌트가 마운트 될 때 할당되며 언마운트 될 때 Null을 할당한다.

다시 말하면, React Component의 인스턴스나, React DOM의 ref 속성에 해당 ref 참조(this.ref)를 선언하지 않으면 그저 가변성을 가진 객체일 뿐이다.

그래서 왜 Ref?

useRef로 관리하는 변수는 가변성을 가지기 때문에 값이 바뀐다고 해서 컴포넌트가 리더링되지 않는다. 리액트 컴포넌트에서의 상태는 setState를 하고 나서 그 다음 렌더링을 거쳐야만 업데이트 된 상태를 조회 할 수 있는 반면, useRef 로 관리하고 있는 변수는 설정 후 바로 조회 할 수 있다.

즉, 변수 업데이트 시 불필요한 렌더링을 피할수 있다.

  • 컴포넌트 안에서 조회 및 수정 할 수 있는 변수
  • setTimeout, setInterval 을 통해서 만들어진 id
  • 외부 라이브러리를 사용하여 생성된 인스턴스
  • scroll 위치

등의 정보를 효율적으로 관리할 수 있다.

useRef

const refContainer = useRef(initialValue);

useRef.current 프로퍼티로 전달된 인자 (initialValue)로 초기화된 변경 가능한 ref 객체를 반환한다. 반환된 객체는 컴포넌트의 전 생애주기를 통해 유지된다.

일반적인 사용예는 자식 컴포넌트에게 명령적으로 접근하는 경우이다.

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current 가 마운트된 text input 엘리먼트를 가리키고있다.
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

본질적으로 useRef.current 프로퍼티에 변경 가능한 값을 담고있는 '상자'와 같다.

useRef()가 순수한 자바스크립트 객체를 생성한 다는 점에서 useRef()
{current: ...} 객체 자체를 생성하는 것의 유일한 차이점이라면 useRef는 매번 렌더링을 할 때 동일한 ref 객체를 제공한다는 것이다.

useRef는 내용이 변경될 때 그것을 알려주지 않는다. 즉 리렌더링 되지 않기 떄문에 ref의 attach나 detach할 때 어떤 코드를 실행하고 싶다면 콜백 ref를 사용해야 한다.

예시) 클래스형 컴포넌트 Ref

React.createRef()

// App.js
import React from "react";
import BucketList from "./BucketList";
import styled from "styled-components";

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

    this.state = {
      list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
    };
    
    // ref 선언
    this.text = React.createRef();
  }
  
  // 처음 로딩이 끝난 뒤 ref
  componentDidMount(){
    console.log(this.text);
    console.log(this.text.current);
  }
  
  addBucketList = () => {
    let list = this.state.list;
    const new_item = this.text.current.value;
    
    // 배열 항목 합치기
    // 1. concat으로 배열 합치기
      // list = list.concat(new_item);
      // this.setState({list: list});
    
    // 2. 스프레드 문법으로 합치기
    this.setState({list: [...list, new_item]});
  }

  
  render() {
    return (
      <div className="App">
        <Container>
          <Title>내 버킷리스트</Title>
          <Line />
          <BucketList list={this.state.list} />
        </Container>
        <div>
          <input type="text" ref={this.text}/>
          <button onClick={this.addBucketList}>추가하기</button>
        </div>
      </div>
    );
  }
}

const Container = styled.div`
max-width: 350px;
min-height: 80vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;

const Title = styled.h1`
color: slateblue;
text-align: center;
`;

const Line = styled.hr`
margin: 16px 0px;
border: 1px dotted #ddd;
`;

export default App;

// BucketList.js
import React from "react";
import styled from "styled-components";

const BucketList = (props) => {
  const my_lists = props.list;
  return (
    <ListStyle>
      {my_lists.map((list, index) => {
        return (
          <ItemStyle key={index}>
            {list}
          </ItemStyle>
        );
      })}
    </ListStyle>
  );
  
const ListStyle = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
`;
  
const ItemStyle = styled.div`
padding: 16px;
margin: 8px;
background-color: aliceblue;
`;
  
export default BucketList;

ref 속에 가변성을 가진 text 변수를 만들어 새로운 값의 입력시 재랜더링없이 text를 먼저 변경시켜 준 뒤 text를 바탕으로 state의 list를 setState해주고 다시 자식 컴포넌트에게 넘겨줘서 랜더링을 하는 방식

예시) 함수형 클래스 Ref

React.useRef()

참조

// App.js
import React from 'react';
import UserList from './UserList';

function App() {
  const [inputs, setInputs] = useState({
    username: '',
    email: ''
  });
  
  const { username, email } = inputs;
  
  const onChange = e => {
    const { name, value } = e.target;
    setInputs({
      ...inputs,
      [name]: value
    });
  };
  const [users, setUsers] = useState([
    {
      id: 1,
      username: 'velopert',
      email: 'public.velopert@gmail.com'
    },
    {
      id: 2,
      username: 'tester',
      email: 'tester@example.com'
    },
    {
      id: 3,
      username: 'liz',
      email: 'liz@example.com'
    }
  ]);

  const nextId = React.useRef(4);
  
  const onCreate = () => {
    // 배열에 항목 추가하는 로직
    const user = {
      id: nextId.current,
      username,
      email
    };
    setUsers([...users, user]);

    setInputs({
      username: '',
      email: ''
    });
    
    nextId.current += 1;
  };
    return (
    <>
      <CreateUser
        username={username}
        email={email}
        onChange={onChange}
        onCreate={onCreate}
      />
      <UserList users={users} />
    </>
  );
}

export default App;

// UserList.js
import React from 'react';

function User({ user }) {
  return (
    <div>
      <b>{user.username}</b> <span>({user.email})</span>
    </div>
  );
}

function UserList({ users }) {
  return (
    <div>
      {users.map(user => (
        <User user={user} key={user.id} />
      ))}
    </div>
  );
}

export default UserList;

// CreateUser.js
import React from 'react';

function CreateUser({ username, email, onChange, onCreate }) {
  return (
    <div>
      <input
        name="username"
        placeholder="계정명"
        onChange={onChange}
        value={username}
      />
      <input
        name="email"
        placeholder="이메일"
        onChange={onChange}
        value={email}
      />
      <button onClick={onCreate}>등록</button>
    </div>
  );
}

export default CreateUser;

0개의 댓글