작동 화면에서 div 리사이징 기능은 react-grid-layout 라이브러리를 이용해서 구현한 것.
사내 프로젝트에서 react-grid-layout 을 이용해 대시보드 페이지의 위젯을 만들면서, 컨테이너 크기에 비례하는 크기를 가지는 텍스트 컴포넌트를 만들어야 했다.
처음엔 미디어쿼리를 이용해서 만들려고 했으나, 브레이크 포인트를 지정하기가 애매한 상황이였고,
react-fittext 라는 라이브러리를 사용하려고 했으나, 요구사항을 모두 충족시키기에 부족한 점이 있어서 직접 구현하기로 했다.
react-resize-detector 라이브러리가 필요하다.
react-resize-detector는 window.addEventListener(”resize”, handleResize);
처럼, 지정한 React Element의 사이즈가 변경될 때 마다 어떤 작동을 수행하거나, width, height 값을 구할 수 있도록 도와주는 라이브러리 이다.
텍스트 영역(<text>
)의 bounding box의 정보(width, height)와 컨테이너 영역(<div>
)의 width, height의 비율을 계산해서, 텍스트 영역(<text>
)을 그 비율만큼 scale 시켜주는 것이다.
svg로 구현하기 때문에 scale 되더라도 이미지가 깨지는 일이 없다.
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
const ResponsiveText = ({ children }) => {
const textRef = useRef(null);
const [ContainerW, setContainerW] = useState(1);
const [ContainerH, setContainerH] = useState(1);
useEffect(() => {
const textElement = textRef.current;
if (textEl) {
// getBBox를 이용해서 <text> 가 가지고있는 bounding box의 정보를 구한다.
// * getBBox는 scale 같은 속성에 영향을 받지 않은 상태의 bounding box를 리턴한다.
const textBox = textElement.getBBox();
const textW = textBox.width;
const textH = textBox.height;
// scale 시킬 비율을 구한다.
const wRatio = ContainerW / textW;
const hRatio = ContainerH / textH;
// 텍스트가 넘치지 않도록 하기 위해
const ratio = wRatio < hRatio ? wRatio : hRatio;
// 좌측 정렬
// const hAlign = 0;
// 가운데 정렬
const hAlign = (ContainerW - textW * ratio) / 2;
// 우측 정렬
// const hAlign = ContainerW - textW * ratio;
// matrix function의 인자 의미는 다음과 같다.
// matrix(scaleX, skewY, skewX, scaleY, translateX, translateY)
textEl.setAttribute(
'transform',
`matrix(${ratio}, 0, 0, ${ratio}, ${hAlign}, 0)`,
);
}
}, [ContainerW, ContainerH]);
// useResizeDetector의 핸들러
// w, h는 ref를 지정한 React Element의 width, height이다.
const onResize = useCallback((w, h) => {
setContainerW(w);
setContainerH(h);
}, []);
// useResizeDetector가 React Element의 사이즈 변경을 감지한다.
const { ref: containerRef } = useResizeDetector({ onResize });
return (
<div ref={containerRef}>
<text x="0" y="0" ref={textRef} fill="#000000">
{children}
</text>
</div>
);
};
svg의 <text>
는 <textarea>
처럼 텍스트가 길어진다고 해서 자동으로 줄을 바꿔주지 않는다.
따라서 ‘\n’(개행문자)와 <tspan>
을 이용해 줄바꿈을 직접 구현해주어야 하는데, 다음과 같이 구현했다.
// 문자열을 개행문자로 구분해 배열을 만들어 준다.
// 배열의 length가 행의 갯수가 된다.
// ex) 'ABC\nDEF\nGHI' => ['ABC', 'DEF', GHI']
const getMultiLineStrArr = (str: string): Array<string> => {
if (str === '') return [''];
const textRows = [];
const textArr = str.split('');
let textRow = '';
for (let i = 0; i < textArr.length; i += 1) {
const text = textArr[i];
if (text !== '\n' && text !== '\r') {
textRow += text;
}
if (text === '\n' || i === textArr.length - 1) {
textRows.push(textRow);
textRow = '';
}
}
return textRows;
};
return (
<div ref={containerRef}>
<text x="0" y="0" ref={textRef} fill="#000000">
// tspan의 상대좌표로 1.2em을 지정함으로써 개행이 이루어진다.
{getMultiLineStrArr(children).map((textRow, idx) => (
<tspan x="0" dy={idx === 0 ? 0 : '1.2em'} key={String(idx)}>
{textRow}
</tspan>
))}
</text>
</div>
);