클래스 컴포넌트와 생명주기 메서드를 이용하여 작업을 하던 기존 방식에서 벗어나 함수형 컴포넌트에서도 더 직관적인 함수를 이용하여 작업할 수 있게 만든 기능 (= class를 작성하지 않고도 state와 다른 react의 기능들을 사용할 수 있게 해주었다.)
⇒ 이래야 컴포넌트가 렌더링 될 때마다 항상 동일한 순서로 Hook이 호출되는 것이 보장된다.
⇒ 이래야 컴포넌트의 모든 상태 관련 로직을 소스코드에 명확하게 보이도록 할 수 있다.
이 두가지 규칙을 강제하는 eslint-plugin-react-hooks
라는 플러그인이 있는데, 이 플러그인은 CRA
에 기본적으로 포함되어 있다.
커스텀 훅은 React의 특별한 기능이라기보다는, 기본적으로 Hook의 디자인을 따르는 관습이다.
컴포넌트에서 반복되는 로직을 함수로 뽑아내서, 쉽게 재사용 되게 할 수 있다.
커스텀 훅은 이름이 use
로 시작하는 자바스크립트 함수이다. 또한 다른 Hook
을 호출할 수 있다.
import { useState, useEffect, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useUserDispatch } from "../../contexts/userContext";
import * as style from "./style";
const LoginContainer = () => {
const [loginDisabled, setloginDisabled] = useState(true);
const [loginInput, setLoginInput] = useState({
id: "",
password: "",
});
const { id, password } = loginInput
const navigate = useNavigate();
const dispatch = useUserDispatch();
const handleLoginClick = async () => {
const response = await fetch("로그인 API 주소", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
id: id,
password: password,
}),
});
const json = await response.json();
if (!response.ok) {
alert(json.message);
resetInput();
} else {
dispatch({ type: "SET_ID", id: json.id });
dispatch({ type: "SET_LOGIN", isLogin: true });
navigate("/nickname");
}
};
const handleButtonChange = () => {
const isNumberPassword = /[0-9]/g;
const isAlphaPassword = /[a-z]/gi; //10~20 영어, 숫자만 허용
id.length >= 6 &&
password.length >= 10 &&
isNumberPassword.test(password) &&
isAlphaPassword.test(password)
? setloginDisabled(false)
: setloginDisabled(true);
};
const resetInput = () => {
setLoginInput({
id: "",
password: "",
});
setloginDisabled(true);
};
return (
<div css={style.containerStyle}>
<input
css={style.inputStyle}
value={id}
onChange={(e) => {
setLoginInput({ ...loginInput, id: e.target.value });
}}
placeholder="아이디를 입력하세요. (6자리 이상)"
onKeyUp={handleButtonChange}
/>
<input
css={style.inputStyle}
type="password"
value={password}
onChange={(e) => {
setLoginInput({ ...loginInput, password: e.target.value });
}}
placeholder="비밀번호를 입력하세요. (영문 숫자 포함 10자리 이상)"
onKeyUp={handleButtonChange}
/>
<button
css={style.loginButtonStyle}
onClick={handleLoginClick}
disabled={loginDisabled}
>
로그인
</button>
<Link css={style.linkStyle} to="/signup">
<button css={style.signupButtonStyle}>회원가입</button>
</Link>
</div>
);
};
export default LoginContainer;
훗날 회원가입, 기타 등등 기능 구현을 생각했을 때, fetch
와 input
관리는 많은 재사용이 될 것 같다. 그러면 위 두개를 커스텀 훅으로 구현해서 useFetch
와 useInput
을 만들어보자!
import { useCallback } from 'react';
const useGet = (url: string) => {
const get = useCallback(async () => {
const response = await fetch(url);
const data = await response.json();
if (!response.ok) {
throw new Error(data.message);
} else {
return data;
}
}, [url]);
return get;
};
const usePost = (url: string) => {
const get = useCallback(async (body: object) => {
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message);
} else {
return data;
}
}, [url]);
return get;
}
export { useGet, usePost };
만약 서버에서 api 요청을 하는 fetch
를 받아올 때, 예상치 못한 오류가 발생하면 자동으로 에러 객체를 반환하도록 하였다.
response.ok
를 검사하는 것의 의미는, 반환 코드가 200~299 사이인지를 검사하는 것으로, 만약 서버, 혹은 클라이언트 오류로 400~599 를 리턴할 때 자동으로 에러를 throw하게 하는 것이다.
그리고 이후 컴포넌트에서 해당 훅을 사용할 때, try-catch로 에러를 검사해 그에 맞는 처리를 할 수 있도록 유도하였다.
import { useState, useCallback } from 'react';
function useInput (initialInput: any) {
const [input, setInput] = useState(initialInput);
const onChange = useCallback((e: any) => {
const { name, value } = e.target;
setInput((input: any) => ({ ...input, [name]: value }));
}, []);
const reset = useCallback(() => setInput(initialInput), [initialInput]);
return [input, onChange, reset];
}
export { useInput };
input
을 관리할 때 필요한 것이 state
, state
변경 함수, state
리셋 함수이다.
따라서 위의 세개를 대신 생성해주는 훅을 구현해, 실제 컴포넌트에서는 이 훅을 호출하는 것만으로 위 세개를 바로 사용할 수 있도록 구현하였다.
import { useState, useEffect, useCallback } from "react";
import { Link, useNavigate } from "react-router-dom";
import { useUserDispatch } from "../../contexts/userContext";
import { usePost } from "../../hooks/useFetch";
import { useInput } from "../../hooks/useInput";
import * as style from "./style";
const LoginContainer = () => {
const [loginDisabled, setloginDisabled] = useState(true);
const [ { id, password }, onInputChange, resetInput ] = useInput({
id: "",
password: "",
})
const navigate = useNavigate();
const dispatch = useUserDispatch();
const postLogin = usePost("로그인 API 주소")
const handleLoginClick = async () => {
try {
const data = await postLogin({id: id, password: password});
dispatch({ type: "SET_ID", id: data.id });
dispatch({ type: "SET_LOGIN", isLogin: true });
navigate("/nickname");
} catch (error) {
alert(error);
resetInput();
}
};
const handleButtonChange = () => {
const isNumberPassword = /[0-9]/g;
const isAlphaPassword = /[a-z]/gi; //10~20 영어, 숫자만 허용
id.length >= 6 &&
password.length >= 10 &&
isNumberPassword.test(password) &&
isAlphaPassword.test(password)
? setloginDisabled(false)
: setloginDisabled(true);
};
return (
<div css={style.containerStyle}>
<input
css={style.inputStyle}
name="id"
value={id}
onChange={onInputChange}
placeholder="아이디를 입력하세요. (6자리 이상)"
onKeyUp={handleButtonChange}
/>
<input
css={style.inputStyle}
type="password"
name="password"
value={password}
onChange={onInputChange}
placeholder="비밀번호를 입력하세요. (영문 숫자 포함 10자리 이상)"
onKeyUp={handleButtonChange}
/>
<button
css={style.loginButtonStyle}
onClick={handleLoginClick}
disabled={loginDisabled}
>
로그인
</button>
<Link css={style.linkStyle} to="/signup">
<button css={style.signupButtonStyle}>회원가입</button>
</Link>
</div>
);
};
export default LoginContainer;
전 코드에 비해 드라마틱한 변화는 없지만, 훨씬 코드가 간결해진 것을 볼 수 있다!