React에서 SVG로 반응형 텍스트 만들기

JS (TIL & Remind)·2022년 2월 15일
1

작동 화면에서 div 리사이징 기능은 react-grid-layout 라이브러리를 이용해서 구현한 것.

개요

사내 프로젝트에서 react-grid-layout 을 이용해 대시보드 페이지의 위젯을 만들면서, 컨테이너 크기에 비례하는 크기를 가지는 텍스트 컴포넌트를 만들어야 했다.

처음엔 미디어쿼리를 이용해서 만들려고 했으나, 브레이크 포인트를 지정하기가 애매한 상황이였고,

react-fittext 라는 라이브러리를 사용하려고 했으나, 요구사항을 모두 충족시키기에 부족한 점이 있어서 직접 구현하기로 했다.

Dependencies

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>
);
profile
노션에 더욱 깔끔하게 정리되어있습니다. (하단 좌측의 홈 모양 아이콘)

0개의 댓글