지난 프로젝트에서 HOC를 이용하여 로그인 여부에 따라 다르게 보여지는 페이지를 구분했다.
이번 포스팅에서는 내가 작성했던 HOC에 코드와 정의에 대해 다시 한 번 정리해보고, 재사용성을 위한 다양한 기능들과의 차이도 알아보려고 한다.
참고 : React 공식문서(Higher-Order Component), 화해 기술블로그
우선 Higher-Order Component는 컴포넌트 로직을 재사용하기 위한 React의 패턴이며 순수 함수라고도 할 수 있다.
컴포넌트를 재사용하기 위한 목적이 큰 만큼, 규모가 커지는 애플리케이션에서 동일한 패턴이 반복적으로 발생할 때 사용하면 유용하다!
고차 컴포넌트 내부에서의 컴포넌트 prototype을 변경하지 않도록 한다.
프로토타입이 변형되어 버린 컴포넌트에 HOC를 적용한다면, 기능이 무시되며 클래스형 컴포넌트와 달리 생명 주기 메서드가 없는 함수 컴포넌트에서도 작동하지 않기 때문에 주의해야 한다.
render 메소드 안에서 고차 컴포넌트를 사용하지 않도록 한다.
render 메소드 안에서 고차 컴포넌트를 사용하게 된다면, 렌더링되며 render가 호출될 때마다 새로운 버전의 컴포넌트가 생성되어 반환된다.
그렇게 되면 매번 전체 트리가 마운트가 해제되고 다시 마운트되는 불필요한 렌더링이 일어나게 된다!
정적 메소드를 사용해야 한다면 반드시 따로 복사해야 한다!
컴포넌트에 HOC를 적용시키고 나면 감싸져 새로 반환된 컴포넌트는 기존 컴포넌트의 정적 메소드를 가지고 있지 않기 때문에, 필요하다면 따로 복사하는 과정이 필요하다!
ref는 전달되지 않기 때문에 필요하다면 React.forwardRef API를 사용한다.
HOC에 ref 값을 추가한다면, 감싸진 컴포넌트가 아니라 가장 바깥쪽 컨테이너 컴포넌트의 인스턴스를 나타내기 때문에 감싸진 컴포넌트를 가리키고 싶다면 React.forwardRef를 사용해야 한다.
HOC와 props render, 컨테이너 컴포넌트 모두 로직을 공유하고 재사용성을 높여주는 목적을 가지고 있지만 서로 다른 관점에서 실행된다..
개념이 헷갈리고 어떤 게 다른 건지 정확하게 알지 못해 답답하여 찾아보게 되었다😰
사용 목적과 정의에 대해 정리해보려고 한다!
[HOC]
🙉 기존 컴포넌트를 파라미터로 받아서 새로운 컴포넌트로 반환하는 함수
- 목적 : 컴포넌트 간에 로직을 추상화하고 재사용성을 높인다.
- 동작 방식 : 주로 파라미터로 받아올 컴포넌트를 감싸는 형태이고, props를 전달하거나 상태 관리의 기능을 추가할 수 있다.
[props.render]
🙉 말 그래도 부모 컴포넌트에서 자식 컴포넌트에 함수를 전달하여 자식 컴포넌트 렌더링을 커스터마이징하는 방법
- 목적 : 특정 상황에 따라 자식 컴포넌트 렌더링을 동적으로 결정하고 싶을 때 사용한다.
- 동작 방식 : 부모 컴포넌트에서 자식 컴포넌트에 함수를 전달하고, 자식 컴포넌트는 그 함수를 실행하여 반환된 결과를 렌더링한다.
[컨테이너 컴포넌트]
🙉 주로 상태 관리 및 데이터 로직과 같은 비즈니스 로직을 처리
- 목적 : 상태 및 데이터 관리와 같은 비즈니스 로직을 분리하여 컴포넌트를 단순화하고 재사용성을 높인다.
- 동작 방식 : Redux, MobX 등 상태 관리 라이브러리와 함께 사용되며, 데이터를 가져오거나 수정하는 등의 작업을 처리한다.
어떤 상황에서 선택해야 할까?
컴포넌트 간의 로직을 공유해야 하거나, 여러 컴포넌트에 동일한 기능을 적용해야 한다면 혹은 cross-cutting concerns(교차 문제)를 해결해야 한다면 HOC
간단한 코드이거나 같은 기능을 제공하되 코드량을 줄이고 싶다면 간편한 props.render
상태 관리나 데이터 로직을 처리할 때, 주로 Redux와 함께 사용되며 전역 상태를 효과적으로 관리해야 한다면 컨테이너 컴포넌트
를 사용한다.
이전에 했던 프로젝트에서 로그인 여부에 따라 구분해야 하는 컴포넌트가 여러개였다.
하나의 파일을 만들어 두고 로그인 구분을 해야 하는 컴포넌트를 감싼다면, 더욱 편할 것 같아서 Higher-Order Component를 만드는 것으로 결정했다.
import { useLocation } from 'react-router-dom';
import RequestLogin from '@components/MyPage/requestLogin';
import WarningMention from '@components/common/warning';
import { getAccessToken } from '@infra/api/token';
const withAuth =
<T extends object>(Component: React.ComponentType<T>) =>
(props: T) => {
const location = useLocation();
const loginToken = getAccessToken();
return loginToken ? (
<Component {...props} />
) : location.pathname === '/my-page' ? (
<RequestLogin />
) : (
<WarningMention text="해당 기능을 사용하시려면 로그인해주세요!" />
);
};
export default withAuth;
우선 제네릭을 활용하여 withAuth 함수가 감쌀 컴포넌트의 타입을 결정했는데, 모든 객체 타입을 허용하도록 T extends object
로 작성했다.
React.ComponentType<T>
를 통해 컴포넌트를 받아와서 HOC를 생성하는 함수임을 나타냈다.
그리고 나는 화살표 함수를 2번 작성했는데, 내부 함수 props: T
를 통해 withAuth에 전달한 props도 받아올 수 있도록 했다.
props가 반환되는 컴포넌트의 핵심 코드(컴포넌트의 내용)이라고도 할 수 있다!
이렇게 만든 withAuth HOC 함수는 다음과 같이 사용했다.
/*좋아요 페이지에 대한 컴포넌트 */
const LikePage = () => {
// ...생략...
}
export default withAuth(LikePage);
이렇게 되면 LikePage 컴포넌트에 정의해놓은 코드구조가 withAuth HOC에 넘어가게 되고, 로그인 여부 로직을 확인한 뒤 알맞은 컴포넌트로 반환되도록 했다.
그런데 다시 보면 짧은 코드지만 가독성이 떨어지는 것 같다..
조건에 대한 삼항연산자가 2번이나 이어져서 더욱 그렇게 느껴진다.
React.FC
로 작성하는 건 어떨까?const withAuth =
<T extends object>(Component: React.FC<T>) =>
(props: T) => {
const location = useLocation();
const hasLoginToken = Boolean(getAccessToken());
if (location.pathname === '/my-page' && !hasLoginToken) {
return <RequestLogin />
}
return hasLoginToken ? (
<Component {...props} />
) : (
<WarningMention text="해당 기능을 사용하시려면 로그인해주세요!" />
);
};
export default withAuth;
크게 차이는 없지만... 이렇게도 작성이 가능할 것 같아 간단하게 수정해보았다!
[props.render]
const AuthRender = ({ render, ...props }) => {
const hasLoginToken = Boolean(getAccessToken());
if (location.pathname === '/my-page' && !hasLoginToken) {
return <RequestLogin />;
}
return hasLoginToken ? render(props) : <WarningMention text="해당 기능을 사용하시려면 로그인해주세요!" />;
}
---
// AuthRender을 사용할 컴포넌트
const RenderComponent = () => {
<AuthRender render={(props) => (
<>로그인 후 보여줄 컴포넌트를 넣거나 정의하기</>
)} />
}
[컨테이너 컴포넌트]
const withAuthContainer = ({ isLogin, children, login }) => {
useEffect(() => {
// 비동기적으로 로그인 여부를 확인하거나 필요한 경우 상태 업데이트!
}, []);
if (location.pathname === '/my-page' && !hasLoginToken) {
return <RequestLogin />;
}
return hasLoginToken ? render(props) : <WarningMention text="해당 기능을 사용하시려면 로그인해주세요!" />;
}
/* Redux와 연결하기 */
// 스토어 상태를 받아와 isLogin에 state.auth.isLogin 값 할당해놓기
const mapStateToProps = (state) => ({
isLogin: state.auth.isLogin,
});
// 액션 생성자 함수인 login이라는 것이 있다고 할 때, 컨테이너 컴포넌트의 Props로 매핑해놓기
const mapDispatchToProps = {
login,
}
// connect 함수를 사용하여 Redux와 React 컴포넌트를 연결한다.
// connect는 Redux에서 HOC를 만드는 패턴이라고도 말할 수 있다. 그러나 거의 사용할 일은 못 본 것 같다...
// mapStateToProps 상태와 mapDispatchToProps 액션 생성자 함수를 withAuthContainer의 props로 보낸다!
const AuthContainer = connect(mapStateToProps, mapDispatchToProps)(withAuthContainer);
---
// AuthContainer를 사용할 컴포넌트
const ContainerComponent = () => {
<AuthContainer>
<>로그인 후 보여줄 컴포넌트를 넣거나 정의하기</>
</AuthContainer>
}
내가 작성했던 HOC를 바탕으로 props.render와 컨테이너 컴포넌트의 코드로 바꾸면, 뭔가 이런식으로 바뀌지 않을까 싶어 한 번 정리해봤다.
오늘 포스팅한 3가지 개념은 아직 나한테 어렵게 느껴진다..!
프로젝트에서도 유용하게 활용하여 이해할 수 있도록 노력해야겠다😊