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>

좋은 웹페이지 즐겨찾기