작성중
NPM 라이브러리로 만들기 위해서 Masonry Gallery UI
를 구현 하기 때문에, DX
를 위해서 Masonry Gallery UI
를 사용 하기 위해서 다른 Provider 또는 다른 컴포넌트를 사용하지 않으려고 한다
Gallery
에서 하위 Image 컴포넌트
에 접근해서 Image Height
에 맞춰서 Masonry Gallery UI Layout
을 변경 해야한다.
Gallery.Image
등을 통해서 Gallery 에서 사용하는 Image
라는 것을 명시적으로 표현 할 수 있게 하고, 다른 컴포넌트를 Import
하지 말자Gallery.Image
에서 GalleryContext
의addItemRefs
를 사용해서 상위 Gallery
컴포넌트의 상태 itemRefs
에 ItemRef
들을 추가한다.
itemRef
는 각 Image
의 ref
들을 갖고 있는 배열 이며, 상위 Gallery
에서 각 이미지 값
에 접근해서 Height
를 얻을 수 있도록 한다.
Gallery.Image
가 렌더링 되면, addItemRefs
를 실행 시켜 Image
의 ref
값을 업데이트 시킨다.
Image Height
에 따라서 Gallery 의 Layout 을 변경 시킨다
Progressive Image
IntersectionObserver API
사용Masonry Layout 로직
해당 로직은 각 이미지가 로드 될 때, 업데이트 되도록 한다.
export const getGridRowEnd = (containerStyle: CSSStyleDeclaration, element: HTMLElement) => {
const columnGap = parseInt(containerStyle.getPropertyValue('column-gap'))
const autoRows = parseInt(containerStyle.getPropertyValue('grid-auto-rows'))
const captionHeight = element.querySelector('.caption')?.scrollHeight ?? 0
const imageHeight = element.querySelector('.figure')?.scrollHeight ?? 0
const spanValue =
captionHeight > 0
? Math.ceil((imageHeight + captionHeight) / autoRows + columnGap / autoRows) - 5
: Math.ceil((imageHeight + captionHeight) / autoRows + columnGap / autoRows)
return `span ${spanValue}`
}
Masnory Gallery UI Wrapper CSS
--gap: 10px;
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(4, 1fr);
column-gap: var(--gap);
grid-auto-rows: var(--gap);
@media screen and (max-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}
@media screen and (max-width: 720px) {
grid-template-columns: repeat(2, 1fr);
}
@media screen and (max-width: 400px) {
display: block;
width: 100%;
}
const initialValue: GalleryContextType = {
addItemRefs: () => {},
handleLayout: () => {},
}
const GalleryContext = createContext(initialValue)
export const Gallery = ({ children, isMobile }: PropsWithChildren<GalleryProps>) => {
const ref = useRef<HTMLDivElement>(null)
const [itemRefs, setItemRefs] = useState<React.RefObject<HTMLAnchorElement>[]>([])
const handleLayout = useCallback(() => {
if (isMobile) return
itemRefs.forEach((itemRef) => {
if (!itemRef.current || !ref.current) {
return
}
const masonryContainerStyle = getComputedStyle(ref.current)
itemRef.current.style.gridRowEnd = getGridRowEnd(masonryContainerStyle, itemRef.current)
})
}, [ref, itemRefs, isMobile])
// Trade-off between UX and Performance
const debouncedFunction = useDebouncedCallback(handleLayout, 200)
useWindowResize(debouncedFunction, [ref.current, itemRefs, debouncedFunction])
useEffect(() => {
handleLayout()
}, [ref, itemRefs, debouncedFunction])
return (
<GalleryContext.Provider
value={{
addItemRefs: (entitiy) => setItemRefs((prev) => [...prev, entitiy]),
handleLayout: debouncedFunction,
}}
>
<ImageContainer size={isMobile ? 'mobile' : undefined} ref={ref}>
{children}
</ImageContainer>
</GalleryContext.Provider>
)
}
Gallery.Image = GalleryImage
const initialValue: GalleryContextType = {
addItemRefs: () => {},
handleLayout: () => {},
}
addItemRefs
: 이미지 컴포넌트 가 렌더 될 때 해당 이미지의 Element 를 추가하기 위한 함수handleLayout
: 이미지가 로드 완료 될 때 Layout을 변경 시키는 함수를 실행const handleLayout = useCallback(() => {
if (isMobile) return
itemRefs.forEach((itemRef) => {
if (!itemRef.current || !ref.current) {
return
}
const masonryContainerStyle = getComputedStyle(ref.current)
itemRef.current.style.gridRowEnd = getGridRowEnd(masonryContainerStyle, itemRef.current)
})
}, [ref, itemRefs, isMobile])
레이아웃을 변경 시키는 함수
이미지를 감싸는 컴포넌트와 하위 이미지들의 스타일 속성들을 통해서 이미지의 gridRowEnd
의 값을 이미지에 맞게 업데이트
// Gallery.Image, It can be setted in <Gallery></Gallery>
const GalleryImage = ({ src, children, href }: React.ImgHTMLAttributes<HTMLImageElement> & { href?: string }) => {
const { addItemRefs, handleLayout } = useContext(GalleryContext)
const [imageSrc, setImageSrc] = useState<string>()
// Ref for Gallery Entities State
const ref = useRef<HTMLAnchorElement>(null)
// Ref for Intersection Obeserver
const imageRef = useRef<HTMLImageElement>(null)
const [entry, observer] = useIntersectionObserver(imageRef)
// For Progressive image
useEffect(() => {
if (entry?.isIntersecting) {
const target = entry.target as HTMLImageElement
setImageSrc(target.dataset.src)
observer?.unobserve(entry.target)
// After Image Content Loaded, handleLayout
handleLayout()
}
}, [entry, observer])
useEffect(() => {
addItemRefs(ref)
}, [ref])
return (
<AnchorContainer href={href} ref={ref} className="masonry-item">
<Figure className="figure">
<Image
ref={imageRef}
className={children ? 'img' : 'img no-caption-img'}
data-src={src}
src={imageSrc}
isLoading={false}
/>
<figcaption className={children ? 'caption' : ''}>{children}</figcaption>
</Figure>
</AnchorContainer>
)
}
import { Gallery } from '@jaewoong2/dui'
const MasonryGallery = () => (
<Gallery>
<Gallery.Image href="/" src={shortSrc}>
<div>Hello</div>
</Gallery.Image>
<Gallery.Image src={longSrc}>
<div>Hello</div>
<div>Hello</div>
<div>Hello</div>
</Gallery.Image>
<Gallery.Image src={shortSrc} />
<Gallery.Image src={longSrc} />
<Gallery.Image src={shortSrc} />
<Gallery.Image src={longSrc} />
<Gallery.Image src={shortSrc} />
</Gallery>
)
안녕하세요. 블로그 글 잘읽었습니다. 감사합니다.
다름이 아니라 nextjs에서 npm으로 해당 '@jaewoong2/dui' 설치하여 사용하려고하는데
이미지랑 모두 잘나오는데 페이지를 처음 로드할때 브라우저 콘솔에 에러가 뜨고
새로고침시 동일한 에러가 계속 나오고있습니다.
react-dom.development.js:17497 Uncaught Error: onload is not defined
at updateDehydratedSuspenseComponent (react-dom.development.js:17497:19)
at updateSuspenseComponent (react-dom.development.js:17193:16)
at beginWork$1 (react-dom.development.js:18509:14)
at beginWork (react-dom.development.js:26927:14)
at performUnitOfWork (react-dom.development.js:25748:12)
at workLoopSync (react-dom.development.js:25464:5)
at renderRootSync (react-dom.development.js:25419:7)
at performConcurrentWorkOnRoot (react-dom.development.js:24504:74)
at workLoop (scheduler.development.js:256:34)
at flushWork (scheduler.development.js:225:14)
at MessagePort.performWorkUntilDeadline (scheduler.development.js:534:21)
이라는 에러가 발생합니다.
해결방법 있을까요??