-
Notifications
You must be signed in to change notification settings - Fork 1
고차 컴포넌트를 활용한 접근 제어
현재 서비스 상, 로그인이 되어 있지 않을 때 접근을 제어해야 하는 컴포넌트가 상당히 많다.
예컨대, 아래와 같이 사이드바의 내 투자, 내 관심 컴포넌트는 로그인이 되어야 이용할 수 있는
서비스이므로 로그인이 되어 있지 않으면 접근을 제어해야 한다.
그런데, 접근 제어를 할 때 현재 컴포넌트 로직의 치명적인 오류를 발견하였다.
현재, 내 투자 컴포넌트의 로직은 아래와 같다.
function MyInvestment() {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
**const { data } = useMyAccount(); // 문제 발생**
const balanceMarketList = useMemo(
// 생략
);
const { sseData } = useSSETicker(balanceMarketList);
const formatters = formatData('KRW');
**if (!isAuthenticated) return <NotLogin size="sm" />;**
return (
// 생략
);
}
export default MyInvestment;
isAuthenticated → 로그인이 유무를 알려주는 변수
useMyAccount() → 내 계좌의 투자 종목을 서버로부터 요청하는 API를 포함한 훅
이때 가장 중요한 것은, 해당 API는 인증과 관련되어 있으므로 Header에 엑세스 토큰을 실어 보낸다.
로직은 아래와 같다.
결론은, 만약 로그인이 되어 있지 않으면 엑세스 토큰도 없고, 리프레시 토큰도 없으므로
홈으로 리다이렉팅 되는 것이다.
문제의 발생 이유는 위의 컴포넌트 로직의 문제점은 선언 순서에 있다.
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
**const { data } = useMyAccount(); // 문제 발생
if (!isAuthenticated) return <NotLogin size="sm" />;**
바로, if 문이 useMyAccount 아래에 있기 때문에, 로그인이 되어 있지 않을 때
접근을 제어해줄 NotLogin 컴포넌트가 렌더링 되기 전에 useMyAccount 훅을 실행하기 때문이다.
그렇다고 아래와 같이 순서를 바꿀 수는 없다.
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
**if (!isAuthenticated) return <NotLogin size="sm" />;
const { data } = useMyAccount(); // 불가능**
리액트는 훅은 분기문(if문) 밑에서 사용할 수 없다. 반드시 if문 상단에서 사용해야 하므로 위의
로직은 불가능한 것이다.
따라서, 다음과 같은 문제를 해결하기 위해 고차 컴포넌트(HOC)를 도입했다.
💡고차 컴포넌트(Higher-Order Component, HOC)는 컴포넌트를 매개변수로 받아서 새로운 컴포넌트를 반환하는 함수로, 컴포넌트 로직을 재사용하기 위한 React 의 패턴 중 하나이다.
function withAuthenticate<P extends object>({
WrappedComponent,
size = 'sm',
}: WithAuthProps<P>) {
return function AuthenticatedComponent(props: P) {
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
if (!isAuthenticated) return <NotLogin size={size} />;
return <WrappedComponent {...props} />;
};
}
export default withAuthenticate;
withAuthenticate
함수는 WrappedComponent
를 인자로 받는다. (size 는 중요 x)
그리고 AuthenticatedComponent
리턴 한다.
AuthenticatedComponent
는 로그인이 되어 있지 않다면 NotLogin
컴퍼넌트를 반환하고
로그인이 되어 있다면 인자로 받았던 WrappedComponent
를 반환하는 것이다.
고차 컴포넌트를 활용하여 아래와 같이 로직을 수정하여 해결하였다.
function MyInvestment() {
const { data } = useMyAccount();
const balanceMarketList = useMemo(
// 생략
);
const { sseData } = useSSETicker(balanceMarketList);
const formatters = formatData('KRW');
return (
// 생략
);
}
**export default withAuthenticate({ WrappedComponent: MyInvestment, size: 'sm' });**
실제로, 굳이 고차 컴포넌트를 사용하지 않더라도 해결할 수 있는 방법은 있다.
하지만, 재사용성 측면에서 고차 컴포넌트의 장점이 발휘된다.
앞서, 위의 gif 에서 내 투자 컴포넌트뿐만 아니라 내 관심 컴포넌트도 접근 제어가 필요하였다.
따라서, 내 관심 컴포넌트도 아래와 같이 고차 컴포넌트를 사용하여 접근 제어를 할 수 있다.
function MyInterest() {
const { isLoading, data: myInterest = [] } = useMyInterest();
// 생략
if (isLoading) return null;
return (
// 생략
);
}
**export default withAuthenticate({ WrappedComponent: MyInterest, size: 'sm' });**
위의 두 컴포넌트뿐만 아니라 접근 제어가 필요한 모든 컴포넌트에서
고차 컴포넌트를 재사용 하였다.
import { lazy, Suspense } from 'react';
import withAuthenticate from '@/components/hoc/withAuthenticate';
const OrderBuyForm = lazy(
() => import('@/pages/trade/components/order_form/forms/OrderBuyForm'),
);
const OrderSellForm = lazy(
() => import('@/pages/trade/components/order_form/forms/OrderSellForm'),
);
const OrderWaitForm = lazy(
() => import('@/pages/trade/components/order_form/forms/OrderWaitForm'),
);
**const AuthenticatedBuyForm = withAuthenticate({
WrappedComponent: OrderBuyForm,
size: 'sm',
});
const AuthenticatedSellForm = withAuthenticate({
WrappedComponent: OrderSellForm,
size: 'sm',
});
const AuthenticatedWaitForm = withAuthenticate({
WrappedComponent: OrderWaitForm,
size: 'sm',
});**
/*
생략
*/
export const createOrderTabs = ({
currentPrice,
selectPrice,
}: CreateOrderTabProsp): OrderTabItem[] => {
return [
{
value: '구매',
id: 'buy',
activeColor: 'text-red-500',
component: (
<Suspense>
<AuthenticatedBuyForm
currentPrice={currentPrice}
selectPrice={selectPrice}
/>
</Suspense>
),
},
{
value: '판매',
id: 'sell',
activeColor: 'text-blue-600',
component: (
<Suspense>
<AuthenticatedSellForm
currentPrice={currentPrice}
selectPrice={selectPrice}
/>
</Suspense>
),
},
{
value: '대기',
id: 'wait',
activeColor: 'text-green-500',
component: (
<Suspense>
<AuthenticatedWaitForm />
</Suspense>
),
},
] as const;
};
- 고차 컴포넌트는 로그인 상태 확인과 UI 렌더링 로직을 완전히 분리된 레이어로 관리할 수 있다.
- 즉 컴포넌트는 자신이 로그인 체크를 받는다는 것을 모르고, 순수하게 자신의 로직에만 집중할 수 있다
- 인증에 관련된 로직은 여러곳에서 재사용되기에 추후 기능 추가/수정이 필요할 때 고차컴포넌트만 수정하면 되므로 유지보수가 용이하다
function MyComponent() {
const { isLoggedIn, user } = useAuth();
if (!isLoggedIn) {
return <Navigate to="/login" />;
}
return <div>Protected Content</div>;
}
const MyComponent = () => {
return <div>Protected Content</div>;
}
export default **withAuthenticate**(MyComponent);
- 위 예시는 간략한 예시로 고차컴포넌트를 사용하면 컴포넌트 내부 로직이 더 깔끔해지고, 조건부 렌더링 코드가 컴포넌트 로직과 섞이지 않는다.
- [FE] TailwindCSS @apply
- [FE] 캐러셀 구현
- [FE] 사이드 바 상태관리 도전기
- [FE] axios interceptor로 로그인 필요한 api 개선하기
- [FE] Tanstack Query API 최적화 도전기
- [FE] Tanstack Query로 구현하는 무한 스크롤 차트 도전기
- [FE] 차트 무한 스크롤링 최적화 도전기
- [FE] 차트 실시간 등락 구현 도전기
- [FE] 검색 구현 및 검색 API 호출 최적화 도전기
- [FE] 고차 컴포넌트를 활용한 인증 접근 제어
- [FE] 코드 스플릿팅으로 최적화 도전기
- [BE] Server 생성
- [BE] CI/CD
- [BE] GitAction 학습 정리
- [BE] ssh터널링으로 db연결
- [BE] 배포환경에서 DB 연결 및 테스트 완료
- [BE] https 적용
- [BE] upbit api 연결 및 SSE api
- [BE] SSE 구현
- [BE] SSE 에러
- [BE] redis 설치 및 연동
- [BE] 트랜잭션 락 구현과 최적화
- [BE] Oauth CORS
- [BE] QueryRunner 사용 시 발생한 문제점과 해결방안
- [BE] Git Action 학습 정리
- [BE] NestJS 학습 정리
- [BE] 로그인 기능 및 리프레시토큰
- [BE] 비회원 체험 기능
- [BE] Nginx 학습 정리
- [BE] Mixed Content와 HTTPS 보안 구현하기
- [BE] 매수/매도 로직 구현 및 개선 과정
- [BE] Queue, Load Balancing, Redis