resizable 컴포넌트를 만들어보자!
📌시작하며
최근 제작하고 있는 웹이 한 번에 표시되어야 하는 정보가 많아, 좌측엔 내비게이션을 두고, 우측엔 해당 페이지에 필요한 정보를 두었다.🤔
좌측 내비게이션은 사이즈가 그리 크지 않고, 펼치거나 접을 수 있기 때문에 괜찮지만, 사용자가 가운데 내용을 보면서 정보 컴포넌트를 함께 보고 싶은 경우에는 고정 사이즈를 제공하는 것보다, 사용자의 화면 크기에 따라 직접 정보 컴포넌트를 줄일 수 있었으면 더 좋은 사용자 경험을 만들 수 있을거란 생각이 들었다.
그래서 이번에는 resizable 한 컴포넌트를 만드는 방법을 정리해보고자 한다!
💻커서 종류
사용자가 해당 컴포넌트의 사이즈 조절이 가능하다는 것을 명확히 알기 위해, <-> 모양의 커서를 사용해야 한다. 먼저 mdn 사이트에서 커서 종류를 살펴보았고, col-resize, row-resize, ns-resize 그리고 ew-resize의 형태를 이용하기로 했다.
tailwind에서도 간단하게 class명으로 커서의 형태를 정해주는데, 정말 다양한 종류가 있지만 간단하게 정리해보자면 다음과 같다.
| Tailwind 클래스 | CSS 속성 | 형태 |
|---|---|---|
cursor-col-resize | col-resize | ↕️ |
cursor-row-resize | row-resize | ↔️ |
cursor-ew-resize | ew-resize | ↕️ |
cursor-ns-resize | ns-resize | ↔️ |
참고로, ew 나 ns는 방향을 나타낸다.
- n → North (위쪽)
- s → South (아래쪽)
- e → East (오른쪽)
- w → West (왼쪽)
🖱️마우스 이벤트
resizable 컴포넌트를 만들 때 사용되는 마우스 이벤트도 알아보자.
📌Mousemove
mousemove 이벤트는 마우스 커서가 evnetListener를 가지고 있는 해당 요소 내에서 움직일때 발생한다.
📌MouseDown
mousedown 이벤트는 마우스 혹은 트랙버튼의 버튼이 눌렸을 때 발생한다.
📌MouseUp
mouseup 이벤트는 마우스 혹은 트랙버튼을 누르고 있던 버튼이 놓였을 때 발생한다.
🧩clientX와 ClientY
스크롤이나 화면에서 일어나는 이벤트에서 자주 접하게되는 속성인 clientX와 clientY에 대해 알아보자!
MouseEvent.clientX와 MouseEvent.clientY는 뷰포트 내의 마우스의 위치를 알려준다.
clientX: 뷰포트 내의 수평 좌표clientY: 뷰포트 내의 수직 좌표
⭐주의할 점은, 마우스 위치의 기준이 컴포넌트 (페이지 길이)가 아니라, 뷰포트 라는 것! 즉 페이지가 얼마나 긴지에 상관없이, 뷰포트 좌측 하단은 항상 clientX가 0이라는 사실이다.
👨🏻💻예제
먼저 사이즈가 조절 되어야 하는 자식 요소 두 가지를 만들어 주었다. 사용하고 있는 Container가 모바일 정도의 사이즈라, 좌우가 아닌 상하 로 조절 될 수 있도록 세팅해주었다.
먼저 구분하기 좋게 Top 컴포넌트와 Bottom 컴포넌트를 준비했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
interface DivProps extends HTMLAttributes<HTMLDivElement> {
className?: string
}
const Top = ({ className, ...props }: DivProps) => {
return (
<div className={cn("w-full h-full bg-blue-400", className)} {...props}>
top
</div>
)
}
const Bottom = ({ className, ...props }: DivProps) => {
return (
<div className={cn("w-full bg-pink-400", className)} {...props}>
Bottom
</div>
)
}
이제 부모 요소를 만들어 준다. 사용하고 있는 Container 컴포넌트 안에 Top과 Bottom을 위치시켜주었다.
이때 div로 Top과 Bottom을 구분하기 위한 라인을 만들어 주었고, absolute로 Top컴포넌트 바닥에 딱 맞게 붙여 주었다.
커서는 위에서 살펴본 상하 화살표를 나타내는 cursro-ns-resize를 이용해 마우스나 트랙패드 커서를 해당 라인 가까이에 갔을 때, 사용자에게 해당 컴포넌트의 사이즈를 조절 할 수 있음을 시각적으로 표시하였다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
"use client"
import React, { HTMLAttributes, useState, useRef } from "react"
import Container from "@/components/Container"
import { cn } from "utils/cn"
const Resizable = () => {
/*생략*/
return (
<Container>
<Container.Title>Resizable 컴포넌트</Container.Title>
<div
ref={containerRef}
className="h-[calc(100dvh-150px-12px)] mt-3 w-full bg-red-50"
>
<div style={height: `${topHeight}px`} className="relative w-full">
<Top />
// Top과 Bottom을 구분하기 위한 라인
<div
onMouseDown={handleMouseDown}
className="w-full h-1 absolute bottom-0 left-0 right-0 bg-black cursor-ns-resize"
/>
</div>
<Bottom style={height: `calc(100% - ${topHeight}px)`} />
</div>
</Container>
)
}
export default Resizable
변수 선언부를 살펴보자.
- 기본적으로
Top컴포넌트의 height 값을 300으로 지정해주었다. isResizing을 통해 사용자가 현재 리사이징을 시도하는지 확인한다.containerRef를 통해 resize 이벤트가 일어나는 구간을 확인하고 제한한다.startY와startHeight를 통해 마우스 이벤트에서 일어나는 컴포넌트 크기 조절과 사용자 마우스 위치의 오차를 조정한다.
1
2
3
4
5
const [topHeight, setTopHeight] = useState(300)
const isResizing = useRef(false)
const containerRef = useRef<HTMLDivElement | null>(null)
const startY = useRef(0) // 마우스 클릭 시점의 Y 좌표
const startHeight = useRef(0) // 마우스 클릭 시점의 Top 높이
이벤트를 살펴보자
1
2
3
4
5
6
7
8
9
10
11
12
const handleMouseMove = (e: MouseEvent) => {
if (!isResizing.current || !containerRef.current) return
// 마우스가 이동한 거리
const deltaY = e.clientY - startY.current
let newHeight = startHeight.current + deltaY
if (newHeight < 200) newHeight = 200
if (newHeight > 400) newHeight = 400
setTopHeight(newHeight)
}
- 마우스가 움직일 때마다 호출되어 크기를 조정한다.
- 마우스를 누른 시점의 Y좌표와 뷰포인트 내의 마우스 차이를 계산해 마우스 이동 거리를 구하고,
이전값에 더해 새로운 높이를 계산한다. Height는 200~400의 값을 가질 수 있다. (그 사이에서 리사이징 됨)
1
2
3
4
5
6
7
8
9
10
11
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
isResizing.current = true
// 현재 마우스 위치와 Top의 현재 높이
startY.current = e.clientY
startHeight.current = topHeight
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
}
- 사용자가 마우스를 누르고 있을 때 작동한다.
- 드래그 되는 등의 기본 동작을 방지한다.
- 마우스 클릭 시점의 좌표와 현재 높이를 저장한다.
1
2
3
4
5
const handleMouseUp = () => {
isResizing.current = false
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}
mouseup상황에서는 리사이징을 끝내고. 이벤트 리스너를 제거한다.
✅마무리!
이 과정을 통해 상하로 리사이징 되는 컴포넌트를 만들 수 있었다! 시작하기 전에는 엄청 복잡한 코드일 거라 생각했는데, 생각보다 코드 양이 많지 않았고 더 쉽게 구현할 수 있어 앞으로 필요할 때 적절히 활용하면 좋을 것 같다.
실제 프로젝트에 적용할 수 있을지는 다른 팀원분들과 이야기해봐야겠지만, 도입하지 않더라도, 이렇게 미리 실험해보면서 기능들을 구현해보는 과정이 재밌고 뿌듯하다.😎