nemo = {
column :4,
row : 4,
channelId:'UCKmEDAD5k5KFMcY5wvGIeGQ',
nemoTitle: 'asmr soupe',
isLargerSize: false,
videos:[{...},{...},{...},...]
}
네모의 json구조
그 중 리사이징과 관련된 데이터는 다음과 같다.
가로
,세로
를 결정 짓는 column
,row
를 추가 시켜주었다.
그리고 isLargerSize
는 썸네일의 크기
를 두 가지
로 사용 할 수 있도록 하는 boolean
정보를 담고 있다.
videos
는 재생목록의 25개의 비디오정보
를 포함하고 있다. 25개로 정한것은 네모
의 최대크기
를 column:10
,row:10
으로 정했고 그렇게 했을때 25개의 비디오를 표시할 수 있도록 했기 때문이다.
page.module.css
네모가 모여있는 상위 컴포넌트 Page의 그리드 css값
.grid {
display: grid;
grid-template-columns: repeat(10, 1fr);
grid-template-rows: repeat(auto-fill, 1fr);
gap: 0.5em 1em;
}
//nemo.jsx
//네모의 스타일을 jsx내에서 인라인으로 지정해 nemo의 column,row값에 접근했다.
<Nemo
id={nemo && nemo.nemoId}
className={styles.nemo}
style={{
opacity: isDragging ? '0.3' : '1',
//gridColumn,gridRow에서 값을 각각 `auto/span ${nemo.column || nemo.row}`로 적용
gridColumn: nemo && `auto/span ${nemo.column}`,
gridRow: nemo && `auto/span ${nemo.row}`,
}}
>
//...네모콘텐츠생략
</Nemo>
⬆ 네모의 isLargerSize : 왼쪽이 `false` 오른쪽이 `true`이다. //nemo.jsx
//isLargerSize가 'true'일때 비디오 가 'auto/span 3'을 갖고, 아닐때 'auto/span 2'를 갖도록 하기위한 gridRatio 변수 선언
const gridRatio = isLargerSize ? 3 : 2;
return(
<Nemo
id={nemo && nemo.nemoId}
className={styles.nemo}
style={{
opacity: isDragging ? '0.3' : '1',
gridColumn: nemo && `auto/span ${nemo.column}`,
gridRow: nemo && `auto/span ${nemo.row}`,
}}>
{videos &&
videos.map(
(video, index) =>
//**이 부분은 네모안에 비디오 갯수를 정하는 부분. 네모의 grid column,row를 각각 비디오의 grid column,row값으로 나누어 소수점을 버린후에 곱한 값이 네모안에 표시될 비디오의 갯수이다.
index <
parseInt(nemo.column / gridRatio) * parseInt(nemo.row / gridRatio) && (
<Video
key={index}
video={video}
isLargerSize={isLargerSize} //네모 내부의 비디오 컴포넌트에 isLargerSize 전달 //fontSize: isLargerSize ? `1.5em` : '1.1em'
gridRatio={gridRatio} // 비디오 컴포넌트에서 인라인 스타일 적용을 위함 gridColumn: `auto/span ${gridRatio}`
nemoPlayer={nemoPlayer}
darkTheme={darkTheme}
/>
)
)}
</Nemo>
)
index <parseInt(nemo.column
/ gridRatio
) * parseInt(nemo.row
/ gridRatio
)
네모의 grid column,row
를 각각 비디오의 grid column,row
값으로 나누어 소수점을 버린 후 곱한 값이 네모안에 표시될 비디오의 갯수이다.
ex)네모
의 column:5,row:6
이고 isLargerSize?false
이면 gridRatio
는 2
이므로 parseInt(5/2)
x parseInt(6/2)
= 2 x 3
으로 비디오가 6
개 표시됨.
네모의 column
이 5
이면 비디오의 column
이 2
일때 가로로 2개
가 표시되어야 하고, row
가 6
이면 세로방향으로 3개
가 표시되어야하기 때문이다.
하지만 column과 row를 각각
나누어서 소수점을 버리지 않고
parseInt(5/2 x 6/2)를 하게 되면 비디오가 7개
가 표시되면서 그리드가 파괴
된다.
drag & drop
에서는 dragRef
와 dropRef
가 모두 nemo
에서 이루어졌지만,
drag resizing
에서는 dragRef
는 리사이징 되어야 할 nemo
에서 선언되고, dropRef
는 page
에서 선언되어 page
영역 내에서 드랍되었을 때의 정보
를 토대로 nemo
가 리사이징 된다.
네모
의 width
와 height
을 각각 column
과 row
로 나누어서 column당 너비
,row당 높이
를 구해 드래그된 x,y좌표
값과 비교해 column
과 row
값을 더하고 줄여 주려고 한다.
//nemo.jsx
//1.변경된 그리드값을 네모에 적용해주는 함수.
//throttleGrid는 네모의 grid,column값을 변경해 저장하는 함수이다.
//_.throttle은 lodash 라이브러리의 스로틀함수로 함수가 너무 잦게 실행되는것을 방지해준다.
const throttleGrid = _.throttle((newGrid) => {
const { column, row } = newGrid;
const newNemo = { ...nemo, column: column, row: row };
//saveNemo는 변경된 네모의 값을 state와 firebase RealtimeDB에 저장해주는 함수이다.
saveNemo(newNemo);
}, 100);//100 ms 값을 줘서 아무리 빨리 반복되어 실행되더라도 100ms에 한번만 실행된다. 100ms간격으로 실행된다는 말이다.
//2.리사이징 되는 기준을 잡아줄 width와 height정보를 rect라는 state에 저장.
//useEffect로 rectRef와 nemo가 변경될때마다 width,height값을 rect state값에 저장해주었다.
useEffect(() => {
const width = rectRef.current.clientWidth;
const height = rectRef.current.clientHeight;
setRect({ width, height });
console.log(rect);
}, [rectRef, nemo]);
//3.react dnd의 useDrag함수.
//이전 포스트에서 사용했던 드래그 드랍과 같은 함수
//이번에는 드래그 될때 전달할 정보에 column,row,width,height,
//throttleGrid,isLargerSize, 6가지 데이터를 전달해주었다.
//6가지 데이터를 통해서 page의 useDrop 함수에서 리사이징될 조건을 만들어 줄 것이다.
const [{ isResizing }, resizeRef] = useDrag(
() => ({
//resizeRef에서 발생하는 드래그 아이템을 Resize로 정했다.
type: ItemTypes.Resize,
item: {
column: nemo && nemo.column,
row: nemo && nemo.row,
width: rect && rect.width,
height: rect && rect.height,
throttleGrid,
isLargerSize: isLargerSize,
},
//isResizing은 리사이징중이라는 상태를 boolean으로 리턴해준다.
collect: (monitor) => ({
isResizing: monitor.isDragging(),
}),
}),
[nemo, rect, isLargerSize, throttleGrid]
);
return (
<Nemo
id={nemo && nemo.nemoId}
className={styles.nemo}
style={{
opacity: isDragging ? '0.3' : '1',
gridColumn: nemo && `auto/span ${nemo.column}`,
gridRow: nemo && `auto/span ${nemo.row}`,
}}
>
//...네모 내부 생략
//비디오들이 담겨있는 div의 rect정보를 위해서 rectRef를 추가.
//최상위 div에 rectRef를 선언하고 싶었지만, 거기에는 드래그엔 드랍에 사용되는 previewRef가 사용되어서 불가능했다.
//previewRef는 reactDnD의 ref여서 rect값에 접근이 불가능 했다. 그리고 video를 담고있는 컨테이너가 전체 크기와 많이 다르지않아 무리없었다.
<div ref={rectRef} className={styles.videos}>
...
<Video/>
...
</div>
//리사이즈 버튼 position:absolute로 네모의 오른쪽 하단에 배치함.
//useDrag함수에서 resizeRef로 정한 변수명을 ref에 적용시켜준다.
<button className={`${styles.drag} ${themeClass}`} ref={resizeRef}>
//리사이징 중일 때 버튼에 네모의 컬럼과 로우를 표시해줬다.
{isResizing && `${nemo.column}x${nemo.row}`}
</button>
</Nemo>
);
//page.jsx
//Nemo의 useDrag함수에서 전달해준 정보를 받는 useDrop함수.
const [, resizeDrop] = useDrop(
() => ({
//아이템 타입이 Resize인 드래그만 받겠다는 뜻.
//드래그엔드랍은 ItemTypes.Nemo로 해주었었다.
accept: ItemTypes.Resize,
canDrop: () => false,
//아이템 타입이 Resize인 아이템이 hover 중 일때 실행되는 함수.
hover(item, monitor) {
//Nemo에서 전달해준 6가지 정보를 es6 destructuring을 이용해 할당함.
const {
column,
row,
width: w, //width와 height은 짧게 w와 h 로 바꿔줬다.
height: h,
throttleGrid,
isLargerSize, //썸네일이 크냐 작냐에 따라 네모의 최소 column,row사이즈를 다르게 지정해주기위해 전달했다.
} = monitor.getItem();
//↓ monitor.getDifferenceFromInitialOffset()는 드래그 이벤트가 시작한 지점으로부터
//얼마나 차이가 발생했는지 오브젝트로 반환해준다. ex) {x:140,y:-40}
const { x, y } = monitor.getDifferenceFromInitialOffset();
let [newColumn, newRow] = [column, row];
//컬럼당 너비와 로우당 높이
const [wPerColumn, hPerRow] = [w / column, h / row];
//리사이징의 반응이 너무 민감해서 x,y값에 0.8을 곱해주었다.
const SENS = 0.8;
//새로운 컬럼 x값을 wPerColumn 나눈 값을 반올림해서 더해주면,
//x값이 컬럼당너비의 반이상이 될때 +1의 값을 더하고 마이너스의 경우에도 -1을 더하게된다.
newColumn += Math.round((x * SENS) / wPerColumn);
newRow += Math.round((y * SENS) / hPerRow);
//컬럼과 로우의 최대 값을 10으로 제한.
newColumn > 10 && (newColumn = 10);
newRow > 10 && (newRow = 10);
//네모 내부에 비디오가 한개는 있어야 하기 때문에 최소 컬럼로우의 값을 3과2로 설정
const gridRatio = isLargerSize ? 3 : 2;
newColumn < gridRatio && (newColumn = gridRatio);
newRow < gridRatio && (newRow = gridRatio);
//컬럼과 로우를 수정하는 함수를 쓰로틀처리 하였지만, 컬럼과 로우가 하나도 변경되지 않았다면,
//수정함수를 호출하지않도록한다.
if (newColumn !== column || newRow !== row){
throttleGrid({ column: newColumn, row: newRow });
}
},
}),
[]
);
return(
//페이지영역에 드래그가 드랍될 dropRef를 지정해준다.
//페이지 영역 밖으로 드래그 하면 동작하지 않음. 원하는 영역이 더 넓다면 더 상위 컴포넌트에서 지정해주면 된다. 위에서 선언된 함수도 같이 이동해야함.
<Page ref={resizeDrop}>
<div className={styles.grid}>
{findPage &&
order &&
order.map((chId, index) => (
<Nemo
key={chId}
index={index}
id={chId}
nemo={findPage.nemos[chId]}
...
/>
))}
</div>
</Page>
)
처음에는 리사이징 되게 만들자! 이렇게 간단하게 시작했는데, 그 안의 표시될 컨텐츠 갯수도 설계해야 했고,
그 컨텐츠가 보기 좋게 꽉차도록 보여야 하는 등, 다양한 문제에 부딪혔다.
문제를 해결하며 내 프로젝트에 꽤 만족스러운 드래그 리사이징 기능이 추가되어 만족감이 컸다.
포스팅을 하면서 과연 다른사람들이 내가 하는 설명을 이해할 수 있을지에 대한 의문이 계속 들었다.
나름 그림까지 만들어가며 설명했지만 여전히 설명은 어렵다. 😭
계속해서 포스팅 하며 설명하는 능력을 길러야겠다.💪🏻
그리고 설명하려고 적다보니 변수명이나 코드를 수정하게 되는 것이 좋은 리뷰가 된다.
시간이 될 때 프로젝트 전체를 다시 만들며 리뷰 해 볼 생각이다. 그 때 마다 포스팅은 업데이트 하겠습니다.🤞🏻
해당 프로젝트를 참고하려고 하는데 왜 현재는 페이지가 DND 나 sizing이 모두 적용이 안될까요??