라우팅 기능이 필요한 클라이언트 애플리케이션을 개발하다보면, 때때로 QueryString
을 검증해야 하는 경우가 생깁니다
제가 만든 블로그에서는 특정 주제에 맞는 게시글을 보여주는 페이지가 위 경우에 해당 되었습니다.
해당 페이지는 다음과 같은 url
을 가지게 됩니다.
/posts?category=JavaScript
→ 카테고리가 JavaScript
인 게시글 목록
/posts?tag=NextJS
→ NextJS
태그가 걸려있는 게시글 목록
/posts?search=React
→ React
가 제목에 들어간 게시글 목록
따라서, 해당 게시글 목록 페이지(/posts
)가 category
/tag
/search
라는 key
가 QueryString
에 없다면 이에 대한 핸들링을 해주어야 합니다.
🛠 HOC Pattern
HOC(High Order Component) Pattern
은 인자로 받은 컴포넌트에 공통된 추가 로직을 수행하여 필요한 결과물을 표현할 수 있도록 도와주는 패턴입니다. → React 공식문서 High Order Component
요즘에는 보통 Custom Hook
패턴을 사용하지만, 개인적으로 컴포넌트의 결과물(return)
이 결정되는 로직은 HOC Pattern
을 사용하는 것이 가독성이 좋다고 생각해 활용하고 있습니다.
HOC Pattern
을 사용해 QueryString
을 검증한다면 다음과 같은 형태가 될 것입니다.
function withPostsQueryValidation<P extends object>(
Component: ComponentType<P>
) {
return function WihLoadingComponent({ ...props }: P) {
const [loading, setLoading] = useState(true);
const { query } = useRouter();
const isQueryInValid = !(query.search || query.tag || query.category);
const queryInvalidCallBack = useCallback(() => {
alert(MESSAGE.INVALIDE_ACCESS);
window.location.replace("/");
}, []);
useEffect(() => {
if (isQueryInValid) queryInvalidCallBack();
else setLoading(false);
}, [query, isQueryInValid]);
if (loading) return <Loading />;
return <Component {...props} />;
};
}
export default withPostsQueryValidation;
function withPostsQueryValidation<P extends object>(
Component: ComponentType<P>
) {
return function WihLoadingComponent({ ...props }: P) {
const [loading, setLoading] = useState(true);
const { query } = useRouter();
const isQueryInValid = !(query.search || query.tag || query.category);
const queryInvalidCallBack = useCallback(() => {
alert(MESSAGE.INVALIDE_ACCESS);
window.location.replace("/");
}, []);
useEffect(() => {
if (isQueryInValid) queryInvalidCallBack();
else setLoading(false);
}, [query, isQueryInValid]);
if (loading) return <Loading />;
return <Component {...props} />;
};
}
export default withPostsQueryValidation;
export default withPostsQueryValidation(Posts);
export default withPostsQueryValidation(Posts);
검증 로직을 HOC
내부에 두고, useEffect
가 실행되어 검증이 되기전까지는 LoadingComponent
를 보여주는 형태입니다.
🛠 Custom Hook Pattern
Custom Hook
패턴을 사용하게 된다면, Hook
내부에서 검증로직을 수행하는 HOC
를 반환하는 형태로 구현할 수 있습니다.
앞서, 보여드린 HOC
패턴은 invalidate
시에 실행하는 콜백을 매게변수로 주입할 경우, Hook
과 관련된 로직을 넣을 수 없었습니다.
하지만, Custom Hook
패턴으로 변경할 경우, Hook
을 이용한 콜백을 주입할 수 있습니다.
interface Params {
onInvalidate: () => void;
}
export default function useSearchParamsValidation({ onInvalidate }: Params) {
const { query } = useRouter();
const isQueryInValid = !(query.search || query.tag || query.category);
const SearchParamsValidationWrap = ({
children,
}: {
children: JSX.Element;
}) => {
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isQueryInValid) onInvalidate();
else setLoading(false);
}, [isQueryInValid, onInvalidate]);
if (loading) return null;
return <>{children}</>;
};
return SearchParamsValidationWrap;
}
interface Params {
onInvalidate: () => void;
}
export default function useSearchParamsValidation({ onInvalidate }: Params) {
const { query } = useRouter();
const isQueryInValid = !(query.search || query.tag || query.category);
const SearchParamsValidationWrap = ({
children,
}: {
children: JSX.Element;
}) => {
const [loading, setLoading] = useState(false);
useEffect(() => {
if (isQueryInValid) onInvalidate();
else setLoading(false);
}, [isQueryInValid, onInvalidate]);
if (loading) return null;
return <>{children}</>;
};
return SearchParamsValidationWrap;
}
const Posts = () => {
const router = useRouter();
const onInvalidate = useCallback(() => {
alert(MESSAGE.INVALIDE_ACCESS);
router.push("/");
}, []);
const SearchParamsValidationWrap = useSearchParamsValidation({
onInvalidate,
});
return (
<SearchParamsValidationWrap>
<>...</>
</SearchParamsValidationWrap>
);
};
const Posts = () => {
const router = useRouter();
const onInvalidate = useCallback(() => {
alert(MESSAGE.INVALIDE_ACCESS);
router.push("/");
}, []);
const SearchParamsValidationWrap = useSearchParamsValidation({
onInvalidate,
});
return (
<SearchParamsValidationWrap>
<>...</>
</SearchParamsValidationWrap>
);
};
검증역할을 수행하는 SerachParamsValidationWrap
HOC
를 훅을 통해 반환하고, 해당 컴포넌트가 반환 JSX
를 감싼 후(children
), validation
이 되었을 때만 children
을 반환하는 형태입니다.
🖥 getServerSideProps
앞서 보여드린 HOC
패턴과 Custom Hook
패턴을 사용하는 경우 한 가지 문제가 있습니다.
바로 두 패턴 모두 useEffect
를 사용해 검증을 진행한다는 점입니다.
useEffect
의 내부 로직은 Server-Side
에서 실행되지 않고, Browser
에서 실행됩니다.
따라서, 서버에서 클라이언트에게 전송하는 HTML
파일에는 어떤한 경우에도 Posts
컴포넌트가 아닌 Loading
컴포넌트가 포함되게 됩니다.
결과적으로, useEffect
내부에서 검증 로직이 실행되는 것이 아니라, 서버에서 라우팅 단계에 검증 로직이 실행되도록 변경해야 합니다.
(서버에서 렌더링할 필요가 없을 경우, 변경하지 않아도 상관 없습니다 → SSR x
CSR o
)
Browser
가 아닌 Server-Side
에서 요청(Request)를 검증하기 위해서는 getServerSideProps
를 이용해야 합니다. (when using NextJS)
공식문서에는 getServerSideProps
는 오직 Server-Side
에서만 실행되고, Browser
에서 실행되지 않는다고 나와있습니다.
getServerSideProps
를 사용하는 페이지는 요청을 받았을 때, getServerSideProps
로직이 실행되고, 반환하는 prop
과 함께 페이지를 pre-rendering
합니다. (공식문서 설명)
추가로, getServerSideProps
는 페이지를 Redirect
시키는 기능을 제공하기 때문에, 이를 이용해 QueryString
을 검증하고 이에 따른 Redirect
처리를 구현 할 수 있습니다.
export const getServerSideProps: GetServerSideProps = async ({
req,
res,
query,
}) => {
const isNumberOfKeyValid = verifyNumberOfKeys(Object.keys(query), 1);
const isValueValid = verifyValue(query);
if (!isNumberOfKeyValid || !isValueValid) {
return {
redirect: {
permanent: false,
destination: "/",
},
props: {},
};
}
return {
props: {},
};
};
function verifyNumberOfKeys(keys: string[], validNumber: number) {
const verifiedKeys = ["category", "search", "tag"];
const includedKeys = keys.filter((key) => verifiedKeys.includes(key));
return includedKeys.length === validNumber;
}
function verifyValue(query: ParsedUrlQuery) {
for (const value of Object.values(query)) {
if (!value) {
return false;
}
}
return true;
}
export const getServerSideProps: GetServerSideProps = async ({
req,
res,
query,
}) => {
const isNumberOfKeyValid = verifyNumberOfKeys(Object.keys(query), 1);
const isValueValid = verifyValue(query);
if (!isNumberOfKeyValid || !isValueValid) {
return {
redirect: {
permanent: false,
destination: "/",
},
props: {},
};
}
return {
props: {},
};
};
function verifyNumberOfKeys(keys: string[], validNumber: number) {
const verifiedKeys = ["category", "search", "tag"];
const includedKeys = keys.filter((key) => verifiedKeys.includes(key));
return includedKeys.length === validNumber;
}
function verifyValue(query: ParsedUrlQuery) {
for (const value of Object.values(query)) {
if (!value) {
return false;
}
}
return true;
}
이번 글에서는 QueryString
검증 관련 로직만을 이야기 했지만, HOC
패턴과 getServerSideProps
를 이용해 관리자 검증, 로그인 여부 검증, 토큰 검증 등등 여러 부분에서도 똑같은 방식으로 검증 로직을 적용할 수 있을것입니다.
🍪 with Cookie
다만, 검증과정에서 Client
에서 온 Request
에 포함된 Cookie
가 필요하다면, 이에 대한 세팅이 따로 필요합니다.
getServerSideProps
에서 인자로 받는 context.req.header.cookie
를 통해 Client
의 Request
에 포함된 Cookie
를 가져올 수 있습니다. (문자열 형태이기 때문에, 파싱이 필요할 경우 파싱 로직 적용)
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const cookies = req.headers.cookie;
};
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const cookies = req.headers.cookie;
};
요 Cookie
를 AJAX
통신에 사용해, Token
혹은 Session
기반으로 로그인 한 사용자를 확인할 수 있습니다.