React Ref
Ref란 뭘까?
HTML에서 id를 사용하여 DOM에 이름을 달아 접근하는 것처럼 리액트 프로젝트에서는 ref
(reference)를 사용하여 render 메서드에서 생성된 DOM 노드나 React 엘리먼트에 접근한다.
그렇다면 왜 ref
를 사용하여 DOM에 접근하는 걸까?
id를 사용할 수도 있지만 가급적 사용하지 않고 ref
를 사용하는 이유는 DOM의 id는 유일해야 하는데, 컴포넌트를 여러 번 사용할 경우 중복 id가 발생할 수도 있기 때문이다. ref
는 전역적으로 작동하지 않고 컴포넌트 내부에서만 작동하기 때문에 이러한 문제가 생기지 않는다.
하지만 ref
를 사용하기 전에 ref
를 사용하지 않고도 원하는 기능을 구현할 수 있는지 충분히 고려한 후에 ref
를 사용하는 것이 좋다. ref
를 사용해야 하는 상황은 다음과 같다.
ref를 사용해야 하는 상황(DOM을 직접적으로 건드려야할 때)
- 포커스, 텍스트 선택영역, 혹은 미디어의 재생을 관리할 때.
- 애니메이션을 직접적으로 실행시킬 때.
- 서드 파티 DOM 라이브러리를 React와 같이 사용할 때.
- 스크롤 박스 조작할 때.
- Canvas 요소에 그림을 그릴 때.
ref를 사용하는 방법에는 두 가지가 있다.
1. 콜백 함수를 통한 ref 설정
ref를 달고자 하는 요소에 ref라는 콜백 함수를 props로 전달해 주면 된다. 이 콜백 함수는 ref 값을 파라미터로 전달받아서 함수 내부에서 파라미터로 받은 ref를 컴포넌트의 멤버 변수로 설정해 준다.
<input ref={(ref) => {this.textInput=ref}}>
위와 같이 작성하면 this.textInput은 input 요소의 DOM을 가리킨다.
아래와 같이 작성할 수도 있다.(리액트 공식 문서 참조)
class CustomTextInput extends React.Component {
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = element => {
this.textInput = element;
};
this.focusTextInput = () => {
// DOM API를 사용하여 text 타입의 input 엘리먼트를 포커스합니다.
if (this.textInput) this.textInput.focus();
};
}
componentDidMount() {
// 마운트 되었을 때 자동으로 text 타입의 input 엘리먼트를 포커스합니다.
this.focusTextInput();
}
render() {
// text 타입의 input 엘리먼트의 참조를 인스턴스의 프로퍼티
// (예를 들어`this.textInput`)에 저장하기 위해 `ref` 콜백을 사용합니다.
return (
<div>
<input type="text" ref={this.setTextInputRef} />
<input
type="button"
value="Focus the text input"
onClick={this.focusTextInput}
/>
</div>
);
}
}
콜백 ref에 관한 주의사항
ref 콜백을 인라인 함수로 선언하였다면 ref 콜백은 업데이트 과정 중에 처음에는 null로, 그 다음에는 DOM 엘리먼트로, 총 두 번 호출된다. 이러한 현상은 매 렌더링마다 ref 콜백의 새 인스턴스가 생성되므로 React가 이전에 사용된 ref를 제거하고 새 ref를 설정해야 하기 때문에 일어난다. 이러한 현상은 ref 콜백을 클래스에 바인딩된 메서드로 선언함으로써 해결할 수 있지만 대부분 이런 현상은 문제가 되지 않는다.
2. createRef를 통한 ref 설정
createRef는 리액트 16.3 버전부터 추가되었다. 이 함수를 사용해서 코드를 작성하면 더 적은 코드로 쉽게 사용할 수 있다. 사용법은 아래와 같다.
class RefSample extends Component {
textInput = React.createRef();
handleFocus = () => {
this.textInput.current.focus();
};
render() {
return (
<div>
<input ref={this.textInput} />
</div>
);
}
}
콜백 함수를 사용할 때와 다른 점은 뒷부분에 .current
를 넣어 주어야 한다는 것이다. React.createRef()
로 생성된 ref
는 자신을 전달받은 DOM 엘리먼트를 current
프로퍼티의 값으로서 받는다.
함수형 컴포넌트는 인스턴스가 없기 때문에 함수 컴포넌트에서는 ref Attribute를 사용할 수 없다. 함수형 컴포넌트에서는 useRef
라는 Hook 함수를 사용한다.
컴포넌트에 ref 달기(부모 컴포넌트에게 DOM ref 공개)
컴포넌트에 ref를 달고 부모 컴포넌트에서 자식 컴포넌트의 내부 메서드를 사용할 수 있다.
ScrollBox.js
class ScrollBox extends Component {
scrollToBottom = () => {
const {scrollHeight, clientHeight} = this.box;
this.box.scrollTop = scrollHeight - clientHeight;
}
render() {
const style = {
border:'1px solid black',
height:'300px',
width:'300px',
overflow:'auto',
position:'relative'
};
const innerStyle = {
width: '100%',
height: '650px',
background: 'linear-gradient(white, black)'
}
return (
<div
style={style}
ref={(ref) => {this.box = ref}}>
<div stle={innerStyle}>
</div>
)
}
}
App.js
class App extends Component {
render() {
return (
<div>
<ScrollBox ref={ref => (this.scrollBox = ref)} />
<button onClick={() => this.scrollBox.scrollToBottom()}>
맨 밑으로
</button>
</div>
);
}
}
위 코드에서 주의할 점은 onClick={this.scrollBox.scrollToBottom} 같은 형식으로 작성해도 틀린 것은 아니지만 컴포넌트가 처음 렌더링될 때는 this.scrollBox 값이 undefined
이므로 값을 읽어오는 과정에서 오류가 발생한다. 화살표 함수를 사용하여 새로운 함수를 만들고 그 내부에서 this.scrollBox.scrollToBottom 메서드를 실행하면, 버튼을 누를 때 이미 한번 렌더링을 했기 때문에 오류가 발생하지 않는다.
ref를 사용할 때에는 먼저 ref를 사용하지 않고도 원하는 기능을 구현할 수 있는지 반드시 고려한 후에 ref를 사용하도록 한다. 서로 다른 컴포넌트끼리 데이터를 교류할 때 ref를 사용한다면 이는 잘못 사용된 것이다. 이는 리액트의 사상에 어긋난 설계이고 앱 규모가 커질수록 구조가 꼬여 유지 보수하기가 점점 힘들어진다.
컴포넌트끼리 데이터를 교류할 때는 언제나 데이터를 부모 <-> 자식 흐름으로 교류해야 한다.
reference: