리액트로 회원가입 구현하기 - useRef vs useState
전체 코드
import { Box, Button, Checkbox, CssBaseline, FormControlLabel, Grid, TextField, Typography, } from "@mui/material";
import { Link, useNavigate } from "react-router-dom";
import Container from "@mui/material/Container";
import { useRef } from "react";
import { LocalStorageController } from "../../util/LocalStorageController";
import { FormDataType } from "./type/FormDataType.type";
import React from "react";
export const Register = () => {
const navigate = useNavigate();
// is~~~Valid : input 값의 유효성을 검사하는 상태값.
// 현재는 ""(빈칸)이 아니어야 통과를 할 수 있게 만들었지만 나중에 정규식을 넣어서 검사하도록 하려고 한다.
// 값이 false 이면 html 코드에서 error={!isUserNameValid}, helperText={isUserNameValid ? "" : "필수 입력 항목입니다."}
// 로 경고창이 뜨게 된다.
const isUserNameValid = useRef(false);
const isUserIdValid = useRef(false);
const isPasswordValid = useRef(false);
const isPasswordCheckValid = useRef(false);
// input 의 value 값
const userName = useRef("");
const userId = useRef("");
const password = useRef("");
const passwordCheck = useRef("");
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
// input이 변할때마다 input의 name별로 value와 유효성 검사상태를 업데이트 해줌.
const { name, value } = event.target;
switch (name) {
case "userName":
userName.current = value;
isUserNameValid.current = Boolean(value);
break;
case "userId":
userId.current = value;
isUserIdValid.current = Boolean(value);
break;
case "password":
password.current = value;
isPasswordValid.current = Boolean(value);
break;
case "passwordCheck":
passwordCheck.current = value;
break;
default:
break;
}
// 비밀번호 input 입력창과 비밀번호 확인의 input 입력창의 value 가 같다면 true 반환
if (passwordCheck === password) {
isPasswordCheckValid.current = true;
} else {
isPasswordCheckValid.current = false;
}
};
// 확인 버튼을 누르면 유효성을 체크하고 input의 입력값들(사용자 정보)을 로컬스토리지에 저장해주고
// 로그인 화면으로 이동시켜줌
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (
isUserNameValid &&
isUserIdValid &&
isPasswordValid &&
isPasswordCheckValid
) {
let n_id: number = 0;
let point: string = "50000";
let userData: FormDataType = {
id: n_id.toString(),
userName: userName.current,
userId: userId.current,
password: password.current,
userPoint: point,
};
let userList: FormDataType[] =
LocalStorageController.getLocalStorageList<FormDataType[]>(
"userList"
) ?? [];
if (userList.length > 0) {
n_id = parseInt(userList[userList.length - 1].id) + 1;
userData.id = n_id.toString();
}
userList.push(userData);
LocalStorageController.saveLocalStorage<FormDataType[]>(
"userList",
userList
);
alert("회원가입이 완료되었습니다.");
navigate("/login");
}
};
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box
sx={{
marginTop: 22,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography component="h1" variant="h5">
회원가입
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
name="userName"
required
fullWidth
id="userName"
label="성명"
autoFocus
onChange={handleInputChange}
error={!isUserNameValid}
helperText={isUserNameValid ? "" : "필수 입력 항목입니다."}
/>
</Grid>
<Grid item xs={12} container spacing={2} alignItems="center">
<Grid item xs={9}>
<TextField
required
fullWidth
id="userId"
label="아이디"
name="userId"
onChange={handleInputChange}
error={!isUserIdValid}
helperText={isUserIdValid ? "" : "필수 입력 항목입니다."}
/>
</Grid>
<Grid item xs={3}>
<Button variant="contained">중복확인</Button>
</Grid>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="비밀번호"
type="password"
id="password"
onChange={handleInputChange}
error={!isPasswordValid}
helperText={isPasswordValid ? "" : "필수 입력 항목입니다."}
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="passwordCheck"
label="비밀번호 확인"
type="password"
id="passwordCheck"
onChange={handleInputChange}
error={!isPasswordCheckValid}
helperText={isPasswordCheckValid ? "" : "필수 입력 항목입니다."}
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox value="allowExtraEmails" color="primary" />}
label="약관 동의"
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
확인
</Button>
<Grid container justifyContent="flex-end">
<Grid item>
{<Link to="/login">이미 계정이 있다면?! 로그인 하러 가기</Link>}
</Grid>
</Grid>
</Box>
</Box>
</Container>
);
};
html 코드는 Material UI 에서 제공하는 free template - sign up 사용(https://mui.com/material-ui/getting-started/templates/)
useState와 useRef 는 모두 리액트에서 상태를 관리하는데 사용되는 Hook이다. 하지만 둘은 목적과 사용 방법에서 차이가 있다.
useState()
- useState는 컴포넌트 내부에서 변화하는 값을 저장하고, 이 값을 변경할 때마다 컴포넌트를 리렌더링한다.
- 상태를 변경할 때는 setState() 함수를 사용하며, 상태를 변경하면 해당 컴포넌트와 하위 컴포넌트가 모두 리렌더링된다. - 상태는 컴포넌트에서 변경되는 값이며, 다른 컴포넌트와 공유되지 않는다.
useRef()
- useRef는 DOM 요소에 직접 접근하거나, 특정 값을 기억하고 싶을 때 사용된다.
- useRef로 생성된 객체는 컴포넌트가 리렌더링되어도 계속 유지된다.
- useRef는 일반적으로 상태 변경에 따른 리렌더링과는 상관없이 동작하는 로직을 구현할 때 사용된다.
useState와 useRef 의 차이점
- useState는 상태를 변경할 때마다 리렌더링이 발생하며, useRef는 상태 변경과 관계없이 계속 유지된다.
- useState는 상태 값을 변경할 때마다 새로운 상태를 생성하는 반면 useRef는 기존에 생성된 객체를 계속 사용한다.
- useState는 상태 값이 변경될 때마다 컴포넌트와 하위 컴포넌트가 모두 리렌더링되지만 useRef는 상태 변경과 관계없이 컴포넌트가 리렌더링되어도 기존 값을 유지한다.
- useState는 컴포넌트 내에서 값을 저장하는 용도로 주로 사용되는 반면 useRef는 DOM 요소에 직접 접근하거나, 이전 값을 기억해야 하는 경우에 주로 사용된다.
이 회원가입 기능을 구현하면서 처음에는 useState 로 아래처럼 초기값을 설정하고 사용했는데, 이렇게 하면 input 값이 바뀔 때마다 컴포넌트가 리렌더링이 된다.
const [isUserNameValid, setIsUserNameValid] = useState(false);
const [isUserIdValid, setIsUserIdValid] = useState(false);
const [isPasswordValid, setIsPasswordValid] = useState(false);
const [isPasswordCheckValid, setIsPasswordCheckValid] = useState(false);
const [userName, setUserName] = useState("");
const [userId, setUserId] = useState("");
const [password, setPassword] = useState("");
const [passwordCheck, setPasswordCheck] = useState("");
export const Register = () => {
console.log("rerendering");
이렇게 콘솔로 찍어보면 useState를 사용했을 때는 input 값이 바뀔 때마다 콘솔창에 아래처럼 rerendering 이 계속 찍히는데( = 컴포넌트가 리렌더링 되고 있다는 것)

useRef 로 바꾸면 아래처럼 처음 렌더링 되었을 때만 콘솔창에 찍히고 input 에 값을 입력해도 아무것도 뜨지 않게 된다.

따라서 지금같은 회원가입창의 경우 input 값이 바뀔 때마다 리렌더링이 되는 것은 불필요하기 때문에 useState를 사용하지 않고 useRef 로 바꾸어 주었다.
지금처럼 작은 규모에서는 상관이 없겠지만 사이트의 규모가 커지게 되고, 컴포넌트 트리가 깊거나 복잡해지는 경우에는 불필요한 리렌더링이 발생하여 성능 저하의 원인이 될 수도 있다.
또한, 상태 값이 변경되어도 렌더링이 필요 없는 경우에도 리렌더링이 발생하여 낭비가 될 수 있다.
결론적으로 useState를 사용할 때에는 상태 값이 변경될 때 컴포넌트를 리렌더링해야 하는지에 대해 신중하게 고려해야 한다.