HOC는 Higher Order Component,
HOF는 Higher Order Function으로
두 개념은 클로저로부터 확장된 개념이다.
HOC는 대표적으로 페이지의 권한을 처리할 때 사용한다.
다음과 같이 useEffect 로 토큰을 검사하여 페이지의 권한을 처리하게되면

권한을 검사하는 모든페이지에 똑같은 코드를 작성해야한다.
이렇게되면 나중에 위의 useEffect 문을 수정할 일이 생겼을 때
일일이 페이지마다 들어가서 수정해야 할 것이다.
여기서 useEffect문 만 따로 뽑아서 컴포넌트로 만든 후 import하여 사용하면

유지보수가 훨씬 간편해 질 것이다.
그렇다면 이 useEffect문을 따로 뽑아서 컴포넌트로 만드는
즉 HOC(Higher Order Component)를 만드는 작업을 직접 해보자!
일단 구현할 프로세스는 다음과 같다.
다음 사진의 세 페이지를 왼쪽부터
좌,중간,우측 컴포넌트로 칭하겠다.

좌측 컴포넌트에서 이메일과 비밀번호를 입력하고 로그인하기 버튼을 눌렀을 때
사용자의 권한을 검사하고, 로그인 권한이 있다면
중간 컴포넌트와 같이 alert창을 띄우고, 로그인 완료 페이지로 보내준다(우측 컴포넌트).
하지만 로그인 권한이 없다면
가차없이 다음과 같은 문구를 보여주며 튕겨낸다.

좌측 컴포넌트import { gql, useMutation } from "@apollo/client";
import { useRouter } from "next/router";
import { useState } from "react";
import { useRecoilState } from "recoil";
import { accessTokenState } from "../../src/commons/store";
// mutation 쿼리
const LOGIN_USER = gql`
mutation loginUser($email: String!, $password: String!) {
loginUser(email: $email, password: $password) {
accessToken
}
}
`;
export default function LoginPage() {
const [, setAccessToken] = useRecoilState(accessTokenState);
const router = useRouter();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loginUser] = useMutation(LOGIN_USER);
// 이메일 입력시 state 변경
const onChangeEmail = (event) => {
setEmail(event.target.value);
};
// 비밀번호 입력시 state 변경
const onChangePassword = (event) => {
setPassword(event.target.value);
};
// state담아서 mutation 요청
const onClickLogin = async () => {
// 1. 로그인하기
const result = await loginUser({
variables: {
email,
password,
},
});
const accessToken = result.data.loginUser.accessToken;
console.log(accessToken);
// 2. 유저정보 받아오기
// 3. 글로벌 스테이트에 저장하기
setAccessToken(accessToken);
localStorage.setItem("accessToken", accessToken);
// 4. 로그인 성공페이지로 이동하기
alert("로그인에 성공하였습니다.");
router.push("/23-05-login-check-success");
};
return (
<div>
이메일 : <input onChange={onChangeEmail} type="text" />
<br />
비밀번호 : <input onChange={onChangePassword} type="password" />
<br />
<button onClick={onClickLogin}>로그인하기</button>
</div>
);
}
중간 컴포넌트// @ts-ignore
import { useRouter } from "next/router";
import { useEffect } from "react";
export const withAuth = (Component) => (props) => {
const router = useRouter();
// 권한분기 로직 추가하기
useEffect(() => {
if (!localStorage.getItem("accessToken")) {
alert("로그인 후 이용 가능합니다!!!");
router.push("/23-04-login-check");
}
}, []);
return <Component {...props} />;
};
우측 컴포넌트import { gql, useQuery } from "@apollo/client";
import { withAuth } from "../../src/components/commons/hocs/withAuth";
const FETCH_USER_LOGGED_IN = gql`
query fetchUserLoggedIn {
fetchUserLoggedIn {
email
name
}
}
`;
function LoginSuccessPage() {
const { data } = useQuery(FETCH_USER_LOGGED_IN);
return <div>{data?.fetchUserLoggedIn.name}님 환영합니다!</div>;
}
export default withAuth(LoginSuccessPage);
좌측컴포넌트에서 로그인하기 버튼을 누르면
router.push("/23-04-login-check")에 의해 우측컴포넌트로 넘어간다.
여기서 다음의 빨간 박스에 주목하자.
중간 컴포넌트
우측 컴포넌트
여태 다룬 컴포넌트는 정직하게
해당 컴포넌트에서 작성된 코드들을 실행하거나 보여줬지만
위의 우측 컴포넌트는 변태스럽게도
중간 컴포넌트인withAuth를 import하고 중간 컴포넌트에
자기 자신을 담아(LoginSuccessPage()함수를 담았았지만 이해하기 쉽게 자기 자신이라 하겠다😅)
export 한다.
여기서 wtihAuth(LoginSuccessPage)
이 부분이 어떻게 처리되는지 빠르게 파악하는 방법에 대해 고민을 해봤는데
함수안에 함수가 들어있는 구조는 return하는 부분에 주목하며 보면 쉽게 파악이 되는 것 같았다.
위의 LoginSuccessPage()함수는 결과적으로 다음 코드를 리턴하므로
return <div>{data?.fetchUserLoggedIn.name}님 환영합니다!</div>
다음 코드는
export default withAuth(LoginSuccessPage)
아래와 같이 생각이 생각하면 쉽게 파악된다.
export default withAuth(<div>{data?.fetchUserLoggedIn.name}님 환영합니다!</div>)
당연한 말이겠지만 이때 withAuth()함수의 인자가 LoginSuccessPage()의 리턴 값으로
대체 된다는 소리는 아니다.
그냥 코드를 파악할 때 머릿속으로 return값으로 대체해서 생각하면 파악하기 쉽다는 말이다.
또한 여기서 LoginSuccessPage()함수의
다음코드 const { data } = useQuery(FETCH_USER_LOGGED_IN)
에 의해 만약data.fetchUserLoggedIn.name이 'Kingmo'라면
return 값을 다음과 같이 생각하고 파악하면 더 파악하기 쉬워진다.
export default withAuth(<div>Kingmo님 환영합니다!</div>)
그렇다면 이제 중간 컴포넌트로 넘어가자
중간 컴포넌트는 다음과 같이 우측 컴포넌트보다 훨씬 더 변태스럽고 끔찍한 구조로 되어있다.
이 코드를 파악하기 위해서는 화살표 함수에서 소괄호와 중괄호의 차이에 대해 알고 있어야한다.
화살표 함수 소괄호() vs 중괄호{}에 관한 글
export const withAuth = (Component) => (props) => {
...생략...
return <Component {...props} />;
};
화살표 함수에서 중괄호와 소괄호는 천지차이이다.
다음과 같이 함수의 바디를 중괄호로 감싸면 const aaa = () => {}
따로 리턴문을 작성하지 않는 이상 리턴하지 않지만
const aaa = () => ()처럼 함수의 바디를 소괄호로 감싸면
따로 리턴문을 작성하지 않아도 리턴한다.
이 부분을 고려하여 아래 함수를 쉬운 구조로 바꿔보겠다.
export const withAuth = (Component) => (props) => {
...생략...
return <Component {...props} />;
};
위 코드는 아래의 구조로 바꿀 수 있다.
export const withAuth = (Component) => {
return (props) => {
// 위 에서 생략 했던 부분을 다시 살렸다.
const router = useRouter();
// 권한분기 로직 추가하기
useEffect(() => {
if (!localStorage.getItem("accessToken")) {
alert("로그인 후 이용 가능합니다!!!");
router.push("/23-04-login-check");
}
}, []);
return <Component {...props} />;
}
};
여기서 우측 컴포넌트에서 다음과 같이 보낸 것을 생각하면
export default withAuth(LoginSuccessPage)
아래의 구조로 바꿀 수 있다.
export const withAuth = (Component) => {
return (props) => {
// 위 에서 생략 했던 부분을 다시 살렸다.
const router = useRouter();
// 권한분기 로직 추가하기
useEffect(() => {
if (!localStorage.getItem("accessToken")) {
alert("로그인 후 이용 가능합니다!!!");
router.push("/23-04-login-check");
}
}, []);
return <LoginSuccessPage {...props} />;
}
};
또한 우측 컴포넌트에서 props를 추가로 보내주지 않았기 때문에
여기서 {...props}는 값을 가지고 있지 않다.
props값을 추가로 보내려면 다음과 같이 보내면 된다.
const data = { nickname : "KingKing-mo" }
export default withAuth(LoginSuccessPage)(data)
마침내 중간 컴포넌트는 다음과 같은 구조로 풀어서 생각할 수 있다.
(❗️이대로 대체된다는 소리는 아니다. 그냥 머릿속으로 이렇게 풀어서 생각하면 파악하기 쉽다는 소리다)
export const withAuth = (Component) => {
return (props) => {
const router = useRouter();
// 권한분기 로직 추가하기
useEffect(() => {
if (!localStorage.getItem("accessToken")) {
alert("로그인 후 이용 가능합니다!!!");
router.push("/23-04-login-check");
}
}, []);
return (
<div>Kingmo님 환영합니다!</div>
)
}
};
위 코드처럼 생각하면 accessToken이 없으면
useEffect함수의 조건문으로 다른 페이지로 보내고
accessToken이 있으면 리턴문을 실행한다는 것을 금방 알 수 있다.
HOF는 HOC와 다를 바가 없다.
HOC를 만드면서 HOF를 썼기 때문이다.
둘의 차이를 뽑자면 return 값이 JSX인지 JSX가 아닌지로 구분된다.
기존에 태그의 id값을 넘겨줄 때 event.target.id를 사용하곤 했다.
하지만 이는 고유한 id를 태그에 입력하는 것이기 때문에
예기치 못하게 id가 중복되어 작성되는 경우 오작동 할 수가 있다.
이러한 이유로 HOF를 사용하게 되었고,
HOF를 사용하면 기존에 UI프레임 워크를 사용하면서 발생했던
id가 사라지는 문제도 해결된다.
// 기존의 방법
export default function Aaa(){
const onClickButton = (event) => {
console.log(event.target.id)
}
return <button id={123} onClick={onClickButton}></button>
}
// HOF
export default function Bbb(){
const onClickButton = (id) => (event) => {
console.log(id)
}
return <button onClick={onClickButton(123)}></button>
}
이것도 위에서 언급한 함수의 return 값 생각하기로 쉽게 구조파악이 가능하다.
// HOF
export default function Bbb(){
const onClickButton = (id) => {
return (event) => {
console.log(id)
}
}
return <button onClick={onClickButton(123)}></button>
}
또 여기서 onClick={{onClickButton(123)}은
아래와 같이 return 값으로 쉽게 파악할 수 있다.
return <button onClick={(event) => {console.log(123)}}></button>
잘보고 갑니다