Ref란?
ref는 참조를 뜻하는 reference의 줄임말로 React에서 자주 쓰이는 hook이다.
가장 기본적인 기능은 다른 DOM 요소에 접근해서 참조하여 작업할 수 있게 해주는 것이다.
ref에 대해서 설명하기 전에 기존의 코드에 대해서 먼저 언급을 하고 넘어가겠다.
// 📂 addUser.js
import React, { useState } from "react";
import Card from "../UI/Card";
import Button from "../UI/Button";
import Wrapper from "../Halpers/Wrapper";
import ErrorModal from "../UI/ErrorModal";
import styles from "./AddUser.module.css";
const AddUser = (props) => {
const [enteredUsername, setEnteredUsername] = useState("");
const [enteredAge, setEnteredAge] = useState("");
const [error, setError] = useState();
const addUserHandler = (event) => {
event.preventDefault();
if (enteredUsername.trim().length === 0 || enteredAge.trim().length === 0) {
setError({
title: "Invalid input",
message: "Please enter a balid name and age (non-empty values).",
});
return;
}
if (+enteredAge < 1) {
setError({
title: "Invalid age",
message: "Please enter a valid age (>0).",
});
return;
}
props.onAddUser(enteredUsername, enteredAge);
setEnteredUsername("");
setEnteredAge("");
};
const errorHandler = () => {
setError(null);
};
const usernameChangeHandler = (event) => {
setEnteredUsername(event.target.value);
};
const ageChangeHandler = (event) => {
setEnteredAge(event.target.value);
};
return (
<Wrapper>
{error && (
<ErrorModal
title={error.title}
message={error.message}
onConfirm={errorHandler}
/>
)}
<Card className={styles.input}>
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={enteredUsername}
onChange={usernameChangeHandler}
/>
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type="number"
value={enteredAge}
onChange={ageChangeHandler}
/>
<Button type="submit">Add User</Button>
</form>
</Card>
</Wrapper>
);
};
export default AddUser;
AddUser라는 component는 사용자로부터 username과 age를 받아오는 기능을 맡고 있다.
이 과정에서 state를 사용항여 사용자가 값을 변경할 때마다 얻은 값으로 state를 업데이트하고 two-way binding으로 그 값을 인풋으로 재설정해주기도 했다.
이렇게 관리해도 좋다. 하지만 폼을 제출할 때만 필요한 값을 키를 입력할 때마다 state 업데이트를 한다는 것은 조금 과하다.
이 경우, ref를 사용할 수 있다.
비록 ref의 기능이 이것만 존재하는 것은 아니지만 ref를 이용해서 마지막에 렌더링되는 HTML 요소들과 다른 JavaScript 코드의 연결을 설정할 수 있다.
ref 불러오기
이를 위해서는 먼저 ref를 생성해야한다.
import React, { useRef } from "react";
함수형 component 안에 호출하기만 하면 된다.
모든 React hook과 마찬가지로 useRef도 함수형 component 안에서만 동작한다.
ref는 사용시 해당 ref와 작업할 수 있게 해주는 값을 반환해준다. 다시말해, 요소와 연결해서 해당 요소와 작업할 수 있게 해준다.
const nameInputRef = useRef() ;
이처럼 선언해주고 사용하면 된다.
ref 연결하기
React에 HTML 요소에 ref를 연결하고 싶다고 알려줄 필요가 있다.
특별한 props인 ref을 추가해주면 되는데 ref은 key props처럼 React에서 내장된 속성이다.
<input id="age" type="number" ref={ageInputRef}/>
ref 값 알아보기
실제 연결은 연결을 원하는 HTML 요소에 추가하는데 component가 렌더링되는 JSX 코드로 연결된다.
React가 이 코드에 처음 도달하여 렌더링할 때 nameInputRef에 저장된 값을 Native DOM 요소로 설정한다.
다시 말해, ref 변수에 들어가는 값은 실제 DOM 요소가 될 것이다.
그래서 form이 제출될 때의 handler에서 ref들의 값을 출력해보면 객체 형태로 출력되는 것을 볼 수 있다.
이 객체에는 current 속성이 있는데, 항상 이 값을 가지고 있으며 이 값은 ref가 연결된 실제 값을 가진다.
기본값은 정의되지 않았지만 이 코드가 실행되자마자 ref props으로 연결된 nameInputRef는 값을 가지게 된다.
여기에 저장된 값은 실제 DOM으로 React없이 조작하거나 값을 읽어올 수도 있다.
그래서 current.value 값을 읽어온다면 기록한 값을 읽을 수 있다.
모든 키의 입력을 기록하지 않아도 요소에 접근할 수 있다는 것이 상당히 매력적이다.
코드의 간소화
이 로직을 구현할 때는 제출 버튼이 눌렀을 때 바로 읽어올 수 있기 때문에 state가 굳이 필요없어졌다.
따라서 코드는 조금 더 간소화 할 수 있게 되었다.
import React, { usetate, useRef } from "react";
import Card from "../UI/Card";
import Button from "../UI/Button";
import Wrapper from "../Halpers/Wrapper";
import ErrorModal from "../UI/ErrorModal";
import styles from "./AddUser.module.css";
const AddUser = (props) => {
const nameInputRef = useRef() ;
const ageInputRef = useRef() ;
const [error, setError] = useState();
const addUserHandler = (event) => {
event.preventDefault();
const enteredUsername = nameInputRef.current.value ;
const enteredAge = ageInputRef.current.value ;
if (enteredUsername.trim().length === 0 || enteredAge.trim().length === 0) {
setError({
title: "Invalid input",
message: "Please enter a balid name and age (non-empty values).",
});
return;
}
if (+enteredAge < 1) {
setError({
title: "Invalid age",
message: "Please enter a valid age (>0).",
});
return;
}
props.onAddUser(enteredUsername, enteredAge);
};
const errorHandler = () => {
setError(null);
};
return (
<Wrapper>
{error && (
<ErrorModal
title={error.title}
message={error.message}
onConfirm={errorHandler}
/>
)}
<Card className={styles.input}>
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
ref={nameInputRef}
/>
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type="number"
ref={ageInputRef}
/>
<Button type="submit">Add User</Button>
</form>
</Card>
</Wrapper>
);
};
export default AddUser;
이건 별개의 이야기지만 버튼으로 제출했을 때 기존의 값들을 초기화하는 로직이 state를 사용하지 않음으로써 사라졌다.
앞서서 React 없이 DOM을 조작할 수 있다고 언급했었는데, 일반적인 경우에는 그렇게 하면 안되지만 사용자가 입력한 값을 재설정하려는 경우에는 사용해도 무관하다.
nameInputRef.current.value = '' ;
ageInputRef.current.value = '' ;
위와 같이 빈 문자열로 지정해주면 초기화할 수 있다.
uncontrolled component
추가로 설명하자면, ref를 사용하여 DOM 요소에 접근하는 방법에는 특이한 이름이 존재한다.
여기서는 두개의 Input 요소는 uncontrolled component
라고 불린다.
왜 제어되지 않는다고 할까?
이 요소들은 내부 state이기 때문에 안에서 반영되는 값은 React에 의해 제어되지 않는다.
React의 기능을 사용하는 것은 맞지만 개발자가 가져올 수 있는 것은 사용자가 무언가를 입력하고 그 입력된 값을 가져오는 것이다.
다시말해 React에서는 실제 DOM 요소를 제어하지 않는다는 것을 말한다.
기존의 state를 사용하는 방식과 ref를 사용하는 방식 중 어떤 것을 사용하는 게 나을까?
- 값을 빠르게 읽고 싶을 때는 ref를 사용하는 편이 나을 것이다.
- state는 깔끔한 편이지만 코드를 조금 더 신경써야 한다.
많이 써보면서 어떤 방식이 더 나은지 결정해야겠다.