React HOC vs HOF
HOC
는 Higher Order Component,
HOF
는 Higher Order Function으로
두 개념은 클로저
로부터 확장된 개념이다.
1. HOC
HOC는 대표적으로 페이지의 권한을 처리할 때 사용한다.
다음과 같이 useEffect 로 토큰을 검사하여 페이지의 권한을 처리하게되면
권한을 검사하는 모든페이지에 똑같은 코드를 작성해야한다.
이렇게되면 나중에 위의 useEffect 문을 수정할 일이 생겼을 때
일일이 페이지마다 들어가서 수정해야 할 것이다.
여기서 useEffect문 만 따로 뽑아서 컴포넌트로 만든 후 import하여 사용하면
유지보수가 훨씬 간편해 질 것이다.
그렇다면 이 useEffect문을 따로 뽑아서 컴포넌트로 만드는
즉 HOC(Higher Order Component)를 만드는 작업을 직접 해보자!
일단 구현할 프로세스는 다음과 같다.
다음 사진의 세 페이지를 왼쪽부터
좌,중간,우측 컴포넌트로 칭하겠다.
좌측
컴포넌트에서 이메일과 비밀번호를 입력하고 로그인하기
버튼을 눌렀을 때
사용자의 권한을 검사하고, 로그인 권한이 있다면
중간
컴포넌트와 같이 alert창을 띄우고, 로그인 완료 페이지로 보내준다(우측
컴포넌트).
하지만 로그인 권한이 없다면
가차없이 다음과 같은 문구를 보여주며 튕겨낸다.
HOC 구현 및 이해하기
좌측
컴포넌트
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
이 있으면 리턴문을 실행한다는 것을 금방 알 수 있다.
2. HOF
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>
Author And Source
이 문제에 관하여(React HOC vs HOF), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@kingmo/React-HOC-vs-HOF저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)