next js와 tailwind를 사용해 제작한 mac os 컨셉의 포트폴리오 입니다. 각 프로젝트에 관한 내용을 browser 처럼 열어서 확인 가능하며, 마우스를 통해 움직이거나 크기를 조정하는 동작이 가능합니다.
Vercel을 통한 배포 ▶︎
바로 가기
state를 통해 위치와 크기값이 동적으로 정해지는 전체적인 뼈대가 될 div와 mouse event를 통해 left와 top 값을 변화시킬 div를 만들어 구현했습니다. 기능 구현은 bepyan님 블로그를 참고하였습니다.
function Browser() {
const [browserConfig, setBrowserConfig] = useState({
x: 0,
y: 0,
w: 0,
h: 0,
});
return (
<div className="browser-container">
<div
className="browser-mover browser-topbar"
{...dragMouseDown((intercalX, intervalY) => {
setBrowserConfig({
x: x + intervalX,
t: y + intervalY,
w,
h,
});
})}
></div>
</div>
);
}
onMouseDown 객체를 반환하는 utill 함수를 통해 mouseMove의 시작 위치과 실시간 위치를 빼어 움직인 거리를 인자로 받는 콜백함수에 전달하여 원하는 동작이 가능하도록 구현했습니다.
function dragMouseDown(
onDrag: (intervalX: number, intervalY: number) => void,
stopPropagation?: boolean
) {
if (stopPropagation) e.stopPropagation();
const mouseMoveHandler = (mouseE: MouseEvent) => {
const intervalX = mouseE.screenX - e.screenX;
const intervalY = mouseE.screenY - e.screenY;
onDrag(intervalX, intervalY);
};
const mouseUpHandler = () => {
document.removeEventListener("mousemove", mouseMoveHandler);
};
document.addEventListener("mousemove", mouseMoveHandler);
document.addEventListener("mouseup", mouseUpHandler, { once: true });
}
resize의 경우 x,y 값이 변화하는 만큼 w,h을 증가시키거나 감소시켜 반대편 테두리의 위치를 고정했습니다.
function Browser() {
const [browserConfig, setBrowserConfig] = useState({
x: 0,
y: 0,
w: 0,
h: 0,
});
const inrange = (n: number, min: number, max: number) => {
if (n < min) return min;
if (n > max) return max;
return n;
};
return (
<div className="browser-container">
<div
className="browser-resizer n"
{...dragMouseDown((intercalX, intervalY) => {
setBrowserConfig({
x,
y: inrange(y + interval.y, 0, y + h - MIN_H),
w,
h: inrange(h - interval.y, MIN_H, y + h),
});
})}
></div> {/* x8 (n,s,w,e,nw,ne,sw,se)*/}
</div>
);
}
이미지가 첨부된 contents에 반응형 slide를 구현했습니다. browser의 크기에 따라 slider의 너비와 높이가 변화하고, drag, click 을 통해 이미지를 넘길 수 있습니다. resize observer를 통해 상위 컴포넌트의 크기를 측정하고, custom hook을 통해 mouse event, click event를 정의하여서 동작하도록 구현했습니다.
export default function CarouserlSlide({ images, boundary }: CarouselProps) {
const [size, setSize] = useState({ w: 500, h: 308 });
const slideList = [images.at(-1), ...images, images.at(0)] as string[];
const {
transX,
animation,
currentIndex,
dotHandler,
nextChangeHandler,
prevChangeHandler,
onCarouselDrag,
onTransitionEnd,
} = useCarousel(size.w, slideList.length);
const observer = new ResizeObserver((entries) => {
const ent = entries[0];
const { width, height } = ent.contentRect;
setSize({
w: width,
h: height,
});
});
useLayoutEffect(() => {
if (boundary) {
observer.observe(boundary);
}
}, [boundary]);
return (
<>
<div>
<div
style={{
transform: `translateX(${-currentIndex * size.w + transX}px)`,
transition: `transform ${animation ? 300 : 0}ms ease-in-out 0s`,
}}
onTransitionEnd={onTransitionEnd}
{...onCarouselDrag()}
>
{slideList.map((img, i) => (
<Image
key={i + img}
src={img}
alt={`image${i}`}
width={size.w}
height={size.h}
priority
draggable={false}
/>
))}
</div>
<CarouselButton
buttonMaterials={{ nextChangeHandler, prevChangeHandler }}
/>
<CarouselDot dotMaterials={{ images, dotHandler, currentIndex }} />
</div>
</>
);
}