포트폴리오의 기본은 작성되었으니 한 가지만 더 추가하면 이제 해보고싶었던 것을 할 수 있다는 생각이 든다.
현재 만들어놓은 컴포넌트 트리에서 LeftSide 와 RightSide는 테일윈즈의 XL 해상도에서밖에 보이지 않게 되어 있다.
// app/page.tsx
// tailwindCSS의 xl은 1280px이므로 이 이외에서는 display:none상태인 것.
// hidden 은 display:none을 가리키므로 실제로 html 상에 코드가 기록되는 것은 아니다. 하지만 우리는 이런 부분도 tree shaking을 통해서 더욱 가볍게 만들 수 있지 않은가?
<MotionedDiv
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: delays.SIDE }}
className="hidden xl:inline-flex w-32 h-full fixed left-0 bottom-0"
>
<DynamicLeftSide />
</MotionedDiv>
이를 만들어보려면 간단하게
1.js에서 스크린 사이즈를 구한다.
2. 스크린사이즈가 1280px 이상이라면 tsx에서 해당 컴포넌트를 안나오게 만들어 준다.
3. 컴포넌트는 lazy를 통해서(이건 nextjs니깐 dynamic)으로 청크를 나누어 준다.
까지를 진행하면 될 것 같다.
중간에 꺠달은 사실이지만, 1번부터 'use client'를 사용해야 한다는 것을 간과했다. 그렇다면 원래 코드(server component)와 위 코드 구현된 것의 퍼포먼스까지 비교하면서 진행 가능할 것이다.
//@/lib/hooks/useWindowSizes.ts
// https://usehooks-ts.com/react-hook/use-window-size 참조
import { useState } from 'react';
import { useEventListener, useIsomorphicLayoutEffect } from './index';
interface WindowSize {
width: number;
height: number;
}
export function useWindowSize(): WindowSize {
const [windowSize, setWindowSize] = useState<WindowSize>({
width: 0,
height: 0,
});
const handleSize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
useEventListener('resize', handleSize);
// Set size at the first client-side load
useIsomorphicLayoutEffect(() => {
handleSize();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return windowSize;
}
// @/libs/hooks/useResolutions.ts;
'use client';
import { useEffect, useState } from 'react';
import { useWindowSize } from '.';
import { TAILWIND_XL } from '@/constants/resoultions';
// 1280 px을 잡는다.
export function useResolutions() {
const { width, height } = useWindowSize();
const [isXL, setIsXL] = useState<boolean>(false);
useEffect(() => {
requestAnimationFrame(() => {
if (width >= TAILWIND_XL) {
setIsXL(true);
} else {
setIsXL(false);
}
});
}, [width]);
return {
isXL,
};
}
위 두 hooks를 통해, isXL값에 width 값을 받아
2와 3을 진행한다.
// ./app/page.tsx
const DynamicLeftSide = dynamic(() => import('@/components/leftSide'));
const DynamicRightSide = dynamic(() => import('@/components/rightSide'));
...
const {isXL} = useResolutions()
...
return (
<div>
{isXL && <DynamicLeftSide />}
{isXL && <DynamicRightSide />
</div>
)
이런식으로 lazy loading을 구현할 수 있었다.
내부에서 사용하는 useEventListener, useIsomorphicLayoutEffect의 내용이 흥미로운데, 다시 한번 되짚어 보자.
//@/lib/hooks/useEventListener
//dom 이벤트들을 function overloading해 놓은 모습으로 좋은 참고가 된다.
import { RefObject, useEffect, useRef } from 'react';
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
// MediaQueryList Event based useEventListener interface
// mediaQueryEvent. 미디어쿼리 리스트 오브젝트 이벤트 change를 대상으로 한다.
// https://developer.mozilla.org/en-US/docs/Web/API/MediaQueryList 참조
function useEventListener<K extends keyof MediaQueryListEventMap>(
eventName: K,
handler: (event: MediaQueryListEventMap[K]) => void,
element: RefObject<MediaQueryList>,
options?: boolean | AddEventListenerOptions
): void;
// Window Event based useEventListener interface
// 윈도우 이벤트. resize, scroll, load, unload, hashchange
// 지금 사용하는 부분은 이 타입 constraint에 해당한다.
function useEventListener<K extends keyof WindowEventMap>(
eventName: K,
handler: (event: WindowEventMap[K]) => void,
element?: undefined,
options?: boolean | AddEventListenerOptions
): void;
// Element Event based useEventListener interface
function useEventListener<
K extends keyof HTMLElementEventMap,
T extends HTMLElement = HTMLDivElement
>(
eventName: K,
handler: (event: HTMLElementEventMap[K]) => void,
element: RefObject<T>,
options?: boolean | AddEventListenerOptions
): void;
// Document Event based useEventListener interface
// document event. 전통적인 reactivity를 추가하기 위한 이벤트이다.
// https://www.w3schools.com/jsref/met_document_addeventlistener.asp
// 위 링크처럼, element를 찾아서 이벤트를 붙이는 과정이다.
...
// 이하 hooks 코드
// @/libs/hooks/useIsomorphicLayoutEffect.ts
// 해당 코드는 SSR과 CSR을 구분해서, useEffect를 사용할 것인지 useLayoutEffect를 사용할 것인지를 정리하는 코드이다.
// useLayoutEffect를 사용하는 경우는 전 포스트에서 정리한 바 있으니 간단하게만 정리함
// browser의 렌더링 과정 중 Layout 단계 이후의 paint 단계에 실행되는 것으로, 간단하게 useEffect로 ui 컴포넌트의 디자인 property를 변경하는 경우,
//여러 요청이 중첩되어(데이터를 fetching해 hydrating하는 등)
//compositing 단계로 넘어가지 못하고 계속해서 paint단계에 머물면서 해당 부분이 깜빡거리거나 표시되지 않는 문제가 생기게 된다.
// 그렇기 때문에 render가 끝난 후 실행되는 useEffect가 아니라 paint단계에서 실행되는 위 부분이 필요하다.
// 위와는 별개로, next의 SSR 기반 페이지에서는 페이지가 자바스크립트 번들을 통해 만들어지는 것이 아니다.
// 그리고 nextjs가 만들어놓은 html에 위 layoutEffect코드가 포함될 수 없으므로, 이를 나누어주는 과정이 필요하다.
// https://usehooks-ts.com/react-hook/use-isomorphic-layout-effect
import { useEffect, useLayoutEffect } from 'react';
export const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
내용이 너무 길어졌다.
4-2에서는 모바일 기기인지 브라우저에서 판별하는 내용을 위 useResolutions에 추가해 주고 실제로 돌려서 server components와 tree shaken된 client components의 퍼포먼스를 실제로 구별하는 것까지가 될 것 같다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
모바일 기기 판별은 사실 위 mdn에 이 글귀를 보고 진행하려 했던 것인데
Note: It's worth re-iterating: it's very rarely a good idea to use user agent sniffing. You can almost always find a better, more broadly compatible way to solve your problem!
위의 말대로 이미 해결했던 코드를 다시 확인해 개선하는 것이 확실히 최고의 개발 방법중 하나가 아닐까? 생각한다. 지금 nextjs13으로 개발하는 것처럼 방법은 계속해서 바뀔지 모르지만 결국 더 빠르게, 더 버그 없이, 더 협업을 편하게 할 수 있도록 하는 원칙은 변하지 않기 때문이다.