useRef는 불필요한 렌더링을 막고 싶거나, 변경시 렌더링을 발생시키지 말아야 하는 값을 다룰때 사용합니다.
컴포넌트에서 state가 변경되면, 리렌더링이 일어나고 컴포넌트 내부 변수들은 초기화됩니다.
하지만 ref 안의 값은 변경되어도 렌더링 되지 않고, 컴포넌트 내부 변수들의 값을 유지할 수 있습니다.
또한 state가 변경되어서 리렌더링이 일어나도 ref의 값은 유지됩니다.
useRef는 current 속성을 가지고 있는 ref 객체를 반환합니다.
current 속성은 인자로 받은 초기값을 가지고 있습니다.
const ref = useRef(initialValue)
console.log(ref)
// {current: initialValue}
console.log(ref.current)
// initialValue
useRef는 state setter가 사용되지 않아서 React가 변경감지를 못한다고 상상해보면 아래 코드와 같습니다.
// Inside of React
const useRef = (value) => {
const [ref] = useState({ current: value });
return ref;
}
버튼을 누르면 count가 올라가고 렌더링이 몇 번 진행되었는지 콘솔에 출력하려고 합니다.
const App = () => {
const [count, setCount] = useState(1)
const [renderCount, setRenderCount] = useState(1)
// 무한 루프
useEffect(() => {
// 렌더링 발생하면 useEffect 실행
setRenderCount(renderCount + 1);
// setRenderCount가 렌더링 발생
console.log('render', renderCount);
})
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}+1</button>
</div>
)
}
코드를 실행하면 무한루프에 빠지게 됩니다.
처음 App 컴포넌트가 랜더링이 되면 useEffect() 가 실행됩니다.
useEffect() 가 실행되면 setRenderCount()가 state에 1을 더해 변경합니다.
state가 변경돼서 App 컴포넌트가 랜더링됩니다.
리랜더링 되면서 useEffect()는 다시 실행됩니다.
2 ~ 4번 반복 (무한 루프)
렌더링 횟수를 출력하는 state를 useRef로 바꾸어 보면 무한루프가 해결됩니다.
useEffect() 의 ref가 랜더링을 발생시키지 않기 때문입니다.
const App = () => {
const [count, setCount] = useState(1)
const renderCount = useRef(1)
useEffect(() => {
renderCount.current = (renderCount.current + 1);
console.log('render', renderCount.current);
})
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}+1</button>
</div>
)
}
처음 App 컴포넌트가 랜더링이 되면 useEffect() 가 실행됩니다.
useEffect() 가 실행되면 ref 객체의 current가 가지고 있는 값(1)에 1을 더해서 ref 객체의 current에 초기화
console.log로 출력
useRef는 다른 DOM 요소에 접근해서 그것들로 작업할 수 있게 해줍니다.
React는 모든 컴포넌트에 첨부할 수 있는 특별한 속성을 지원합니다. ref 속성은 콜백 함수를 받고 컴포넌트가 마운트되거나 언마운트 된 이후에 즉시 실행됩니다.
ref 속성을 HTML 요소에서 사용하면, ref 콜백은 기본 DOM 요소를 인수로 받습니다
const myRef = useRef()
<input ref={myRef} />}
console.log(ref)
// {current: input}
반대로 제어된 컴포넌트는 구성될 때 기본적으로 내부 state를 갖으며, 사용자 입력을 받아 저장하고 반영하는 인풋 요소가 구성되어 있습니다.
리액트 앱에서 해당 컴포넌트로 작업할 때 리액트 state를 내부 state에 연결하며 내부 state가 리액트에 의해 제어되기 때문에 이를 '제어된 컴포넌트'라고 합니다.
아래는 이름과 나이를 input으로 입력 받아 버튼으로 제출하는 코드입니다.
input에 키를 누를때마다 state에 저장하여 다시 input에 피드해줍니다.
그리고 피드된 state로 input을 재설정합니다.
submit 버튼을 클릭하면 업데이트된 state를 필요한 곳에 제출합니다.
이 과정에서 키를 입력할 때마다 state는 업데이트 되는데 사실 데이터는 form을 제출할 때만 필요합니다.
import React, { useState } from "react";
import Card from "../UI/Card";
import Button from "../UI/Button";
import classes from "./AddUser.module.css";
const AddUser = (props) => {
// state 사용
const [enteredUsername, setEnteredUsername] = useState("");
const [enteredAge, setEnteredAge] = useState("");
// submit 이벤트 핸들러
const addUserHandler = (event) => {
event.preventDefault();
props.onAddUser(enteredUsername, enteredAge);
setEnteredUsername("");
setEnteredAge("");
};
// 이름 state 변경 이벤트 핸들러
const usernameChangeHandler = (event) => {
setEnteredUsername(event.target.value);
};
// 나이 state 변경 이벤트 핸들러
const ageChangeHandler = (event) => {
setEnteredAge(event.target.value);
};
return (
<>
// 이름 작성 input
<Card className={classes.input}>
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={enteredUsername}
onChange={usernameChangeHandler}
/>
// 나이 작성 input
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type="number"
value={enteredAge}
onChange={ageChangeHandler}
/>
// submit 버튼
<Button type="submit">Add User</Button>
</form>
</Card>
</>
);
};
export default AddUser;
이름과 나이를 state로 관리하고 있지만 ref로도 관리할 수 있습니다.
ref를 사용하여 DOM 요소, 특히 입력 요소와 상호 작용하는 것을 제어되지 않는 컴포넌트라고 합니다.
리액트 state대신 ref를 사용하면 리액트에 의해 제어되지 않기 때문에 '제어되지 않는 컴포넌트'라고 합니다.
useRef를 사용하면 키를 입력할 때마다 state를 업데이트하지 않아도 됩니다.
useRef의 초기값은 필요하지 않기 때문에 정의되지 않았습니다.
하지만 JSX 코드가 랜더링되면서 input의 ref prop으로 인해 useRef와 연결됩니다.
그리고 연결된 useRef가 반환하는 객체의 current 속성에는 연결된 실제 DOM이 저장됩니다.
따라서 useRef으로 실제 DOM을 조작할 수 있습니다.
useRef.current는 input이 되고 useRef.current.value는 input.value가 됩니다.
따라서 useRef.current.value를 state 대신 사용합니다.
import React, { useRef } from "react";
import Card from "../UI/Card";
import Button from "../UI/Button";
import classes from "./AddUser.module.css";
const AddUser = (props) => {
// name input에 입력된 데이터를 nameInputRef의 current에 초기화
// {current: input#username}
const nameInputRef = useRef();
// age input에 입력된 데이터를 ageInputRef의 current에 초기화
// {current: input#age}
const ageInputRef = useRef();
// submit 이벤트 핸들러
const addUserHandler = (event) => {
event.preventDefault();
// state 대신 input 입력값 사용
const enteredName = nameInputRef.current.value;
const enteredUserAge = ageInputRef.current.value;
props.onAddUser(enteredName, enteredUserAge);
// 저장된 input 비우기
nameInputRef.current.value = "";
ageInputRef.current.value = "";
};
return (
<>
// 이름 작성 input
<Card className={classes.input}>
<form onSubmit={addUserHandler}>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
// useRef 연결
ref={nameInputRef}
/>
// 나이 작성 input
<label htmlFor="age">Age (Years)</label>
<input
id="age"
type="number"
// useRef 연결
ref={ageInputRef}
/>
// submit 버튼
<Button type="submit">Add User</Button>
</form>
</Card>
</>
);
};
export default AddUser;
위의 코드에서는 useRef로 DOM을 조작하였지만, 일반적으로 DOM은 리액트에 의해서만 조작되어야 합니다.
위의 코드에서 useRef로 DOM을 조작할 수 있는 이유는 사용자가 입력한 값을 재설정하려는 경우 제외하고는 useRef로 데이터만 읽었을 뿐 실제로 조작이라고 할 행위는 하지 않았기 때문입니다.
실제로 조작이라 함은 우리는 새로운 요소를 추가하거나 CSS 클래스를 변경하는 것을 말합니다.
부모 컴포넌트에서 자녀 컴포넌트로 ref를 전달할 때 사용합니다.
부모 컴포넌트가 fowardRef로 ref를 전달하면 자녀 컴포넌트의 DOM요소에 접근할 수 있습니다.
button을 클릭하면 input에 focus가 되도록 구현하는 코드입니다.
// App.js
function App() {
const inputRef = useRef();
const focus = () => {
inputRef.current.focus();
};
return (
<div>
<input ref={inputRef} />
<button onClick={focus}>포커스</button>
</div>
)
위의 코드에서 input을 재사용 가능한 컴포넌트로 사용하려고 합니다.
MyInput이라는 재사용 가능한 input 컴포넌트를 만들고 inputRef라는 props를 넘겨 사용하면 문제없이 사용할 수 있습니다.
// App.js
import MyInput from './MyInput'
function App() {
const inputRef = useRef();
const focus = () => {
inputRef.current.focus();
};
return (
<div>
<MyInput inputRef={inputRef} />
<button onClick={focus}>포커스</button>
</div>
)
// MyInput
const MyInput = ({inputRef}) => {
return <input ref={inputRef}/>
}
export default MyInput;
하지만 문제가 있습니다.
ref는 prop이 아닙니다.
그래서 ref라는 이름이 아닌 inputRef라는 이름으로 넘기는 것입니다.
만약 ref라는 prop으로 넘겨주려고 한다면 react는 ForwardRef 사용을 권유합니다.
ForwardRef를 사용하면 컴포넌트에 ref라는 이름으로 ref를 넘겨줄 수 있습니다.
사용법은 ref를 넘겨 받을 컴포넌트를 forwardRef의 인자에 넘겨주면 됩니다.
// App.js
import MyInput from './MyInput'
function App() {
const inputRef = useRef();
const focus = () => {
inputRef.current.focus();
};
return (
<div>
<MyInput Ref={inputRef} />
<button onClick={focus}>포커스</button>
</div>
)
forwardRef의 인자에 전달된 컴포넌트는 props 뿐만 아니라 ref라는 인자도 받게 됩니다.
// MyInput
import React, { forwardRef } from 'react'
const MyInput = (props, ref) => {
return <input ref={ref}/>
}
export default forwardRef(MyInput);
하위 컴포넌트의 내부 함수 및 변수를 상위 컴포넌트에서 참조를 통해 접근할 수 있습니다.
useImperativeHandle(상위 컴포넌트의 ref, 외부에서 사용할 수 있는 모든 데이터를 포함하는 객체를 반환하는 함수)
하위 컴포넌트인 Input의 내부에 있는 focus 함수를 상위 컴포넌트에서 사용하려고 합니다.
상위 컴포넌트에서 선언한 ref를 Input으로 전달합니다.
하위 컴포넌트로 전달한 ref의 current를 통해 하위 컴포넌트의 함수를 사용할 수 있습니다.
// Login.js
import React, {
useEffect,
useState,
useReducer,
useContext,
useRef,
} from "react";
import Card from "../UI/Card/Card";
import Button from "../UI/Button/Button";
import AuthContext from "../../store/auth-context";
import Input from "../UI/Input/input";
import classes from "./Login.module.css";
const emailReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.payload, isValid: action.payload.includes("@") };
}
if (action.type === "INPUT_BLUR") {
return { value: state.value, isValid: state.value.includes("@") };
}
return { value: "", isValid: false };
};
const passwordReducer = (state, action) => {
if (action.type === "USER_INPUT") {
return { value: action.payload, isValid: action.payload.trim().length > 6 };
}
if (action.type === "INPUT_BLUR") {
return { value: state.value, isValid: state.value.trim().length > 6 };
}
return { value: "", isValid: false };
};
const Login = (props) => {
const [formIsValid, setFormIsValid] = useState(false);
const [emailState, dispatchEmail] = useReducer(emailReducer, {
value: "",
isValid: null,
});
const [passwordState, dispatchPassword] = useReducer(passwordReducer, {
value: "",
isValid: null,
});
const authCtx = useContext(AuthContext);
// Input 컴포넌트의 useImperativeHandler과 연결한 ref
const emailInputRef = useRef();
const passwordInputRef = useRef();
const { isValid: emailIsValid } = emailState;
const { isValid: passwordIsValid } = passwordState;
useEffect(() => {
const identifier = setTimeout(() => {
setFormIsValid(emailState.isValid && passwordState.isValid);
}, 300);
return () => {
clearTimeout(identifier);
};
}, [emailIsValid, passwordIsValid]);
const emailChangeHandler = (event) => {
dispatchEmail({ type: "USER_INPUT", payload: event.target.value });
};
const passwordChangeHandler = (event) => {
dispatchPassword({ type: "USER_INPUT", payload: event.target.value });
};
const validateEmailHandler = () => {
dispatchEmail({ type: "INPUT_BLUR" });
};
const validatePasswordHandler = () => {
dispatchPassword({ type: "INPUT_BLUR" });
};
const submitHandler = (event) => {
event.preventDefault();
if (formIsValid) {
authCtx.onLogin(emailState.value, passwordState.value);
} else if (!emailIsValid) {
// emailInputRef를 input의 useImperativeHandle와 연결했기 때문에 useImperativeHandle의 focus 사용 가능
emailInputRef.current.focus();
} else {
// passwordInputRef를 input의 useImperativeHandle와 연결했기 때문에 useImperativeHandle의 focus 사용 가능
passwordInputRef.current.focus();
}
};
return (
<Card className={classes.login}>
<form onSubmit={submitHandler}>
<Input
// Input에 ref 전달
ref={emailInputRef}
id="email"
label="E-Mail"
type="email"
isValid={emailIsValid}
value={emailState.value}
onChange={emailChangeHandler}
onBlur={validateEmailHandler}
/>
<Input
// Input에 ref 전달
ref={passwordInputRef}
id="password"
label="password"
type="password"
isValid={passwordIsValid}
value={passwordState.value}
onChange={passwordChangeHandler}
onBlur={validatePasswordHandler}
/>
<div className={classes.actions}>
<Button type="submit" className={classes.btn}>
Login
</Button>
</div>
</form>
</Card>
);
};
export default Login;
forwardRef를 통해 상위 컴포넌트에서 전달한 ref를 받습니다.
useImperativeHandle의 첫 번째 인자에 상위 컴포넌트에서 전달한 ref를 전달합니다.
두 번째 인자에는 상위 컴포넌트에 전달할 activate함수를 focus필드로 전달합니다.
// input.js
import React, { useRef, useImperativeHandle } from "react";
import classes from "./input.module.css";
const input = React.forwardRef((props, ref) => {
// input에 연결한 ref
const inputRef = useRef();
const activate = () => {
inputRef.current.focus();
};
// 상위 컴포넌트의 ref와 연결(emailInputRef, passwordInputRef)
useImperativeHandle(ref, () => {
return {
// activate 함수를 focus필드로 상위 컴포넌트에 전달
focus: activate,
};
});
return (
<div
className={`${classes.control} ${
props.isValid === false ? classes.invalid : ""
}`}
>
<label htmlFor={props.id}>{props.label}</label>
<input
ref={inputRef}
type={props.type}
id={props.id}
value={props.value}
onChange={props.onChange}
onBlur={props.onBlur}
/>
</div>
);
});
export default input;
자녀의 DOM 노드에 접근하는 것은 자녀 컴포넌트가 가지고 있는 DOM 노드를 외부로 노출시키면서 캡슐화를 막는 것이기 때문에 자녀의 DOM노드에 접근이 불가피한, 꼭 필요한 곳에서만 사용하는 것이 좋습니다.
| refs | state |
|---|---|
useRef(initialValue)는 { current: initialValue }을 반환 | useState(initialValue)는 state 변수의 현재값과 state 설정자함수([value, setValue])를 반환 |
| 변경 시 리렌더링을 촉발하지 않음 | 변경 시 리렌더링을 촉발함 |
| Mutable— 렌더링 프로세스 외부에서 current 값을 수정하고 업데이트할 수 있음 | “Immutable”— state setting 함수를 사용하여 state 변수를 수정해 리렌더링을 대기열에 추가해야함 |
| 렌더링 중에는 current 값을 읽거나 쓰지 않아야 함 | 언제든지 state를 읽을 수 있음. 각 렌더링에는 변경되지 않는 자체 state snapshot이 있음 |
일반적으로 ref는 컴포넌트가 React로부터 “외부로 나가서” 외부 API, 즉,컴포넌트의 형상에 영향을 주지 않는 브라우저 API 등과 통신해야 할 때 사용합니다.
컴포넌트에 일부 값을 저장해야 하지만 렌더링 로직에는 영향을 미치지 않는 경우 ref를 선택하세요.
선언적으로 할 수 있는 작업에는 refs 사용을 피하길 바랍니다.