초기에 Trablock에서 로딩 UI 렌더링을 위해 선택한 라이브러리는 react-loader-spinner 입니다. 이 라이브러리에서 제공하는 UI 중 TailSpin이 원하는 UI 형태와 정확히 일치하고, 수정 가능한 여러 옵션을 제공하여 필요에 따라 자유롭게 형태를 변형할 수 있는 것이 큰 장점이었습니다. 일반적인 상황에서는 잘 작동했지만, 페이지 초기 로딩 UI에 적용하니 애니메이션이 작동하지 않는 문제가 발생했습니다. TailSpin의 내부 구현을 살펴보니 다음과 같았습니다.
// ...
<SvgWrapper
style={wrapperStyle}
$visible={visible}
className={wrapperClass}
data-testid="tail-spin-loading"
aria-label={ariaLabel}
{...DEFAULT_WAI_ARIA_ATTRIBUTE}
>
<svg
width={width}
height={height}
viewBox={`0 0 ${viewBoxValue} ${viewBoxValue}`}
xmlns={SVG_NAMESPACE}
data-testid="tail-spin-svg"
>
{/* ... */}
<g fill="none" fillRule="evenodd">
<g transform={`translate(${halfStrokeWidth} ${halfStrokeWidth})`}>
<path
d="M36 18c0-9.94-8.06-18-18-18"
id="Oval-2"
stroke={color}
strokeWidth={strokeWidth}
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite"
/>
</path>
<circle fill="#fff" cx="36" cy="18" r={processedRadius}>
<animateTransform
attributeName="transform"
type="rotate"
from="0 18 18"
to="360 18 18"
dur="0.9s"
repeatCount="indefinite"
/>
</circle>
</g>
</g>
</svg>
</SvgWrapper>
...
로딩 UI를 회전시키는 애니메이션에 SVG의 animateTransform 태그를 사용하고 있었습니다. animate, animateTransform 등은 클라이언트 측에서만 작동하는 태그로, 브라우저의 SVG 렌더링 엔진에 의존합니다. 브라우저가 아직 로드되지 않은 SSR 환경에서는 SVG 렌더링 엔진이 없기 때문에 작동하지 않습니다. 이 때문에 SSR을 사용하는 페이지의 초기 로드에서 사용하면 애니메이션이 작동하지 않았던 것입니다.
SSR 환경에서도 클라이언트 측에서와 동일하게 로딩 UI를 사용하기 위해서는 SVG 렌더링 엔진에 의존하지 않고 서버 측에서도 잘 작동하는 애니메이션이 필요했습니다. 따라서 CSS를 이용해 유사한 형태의 로딩 UI 컴포넌트를 구현했습니다.
function Loading({
className,
width = '100%',
height = '100%',
color = COLORS.WHITE_01,
visible = true,
strokeWidth = 4,
radius = 1
}: LoadingProps) {
const strokeWidthNum = parseInt(String(strokeWidth));
const viewBoxValue = strokeWidthNum + 36;
const halfStrokeWidth = strokeWidthNum / 2;
const processedRadius = halfStrokeWidth + parseInt(String(radius)) - 1;
return (
<div
className={`flex-row-center justify-center ${!visible && 'hidden'} ${className}`}
data-testid="tail-spin-loading"
aria-label="tail-spin-loading"
aria-busy="true"
role="progressbar"
style={{ width, height }}
>
<svg
className="h-full w-full"
viewBox={`0 0 ${viewBoxValue} ${viewBoxValue}`}
xmlns="<http://www.w3.org/2000/svg>"
data-testid="tail-spin-svg"
>
<defs>
<linearGradient x1="8.042%" y1="0%" x2="65.682%" y2="23.865%" id="a">
<stop stopColor={color} stopOpacity="0" offset="0%" />
<stop stopColor={color} stopOpacity=".631" offset="63.146%" />
<stop stopColor={color} offset="100%" />
</linearGradient>
</defs>
<g className="origin-center animate-spin">
<path
d={`M${36 + halfStrokeWidth} ${18 + halfStrokeWidth}c0-9.94-8.06-18-18-18`}
stroke={color}
strokeWidth={strokeWidth}
fill="none"
/>
<circle fill="#fff" cx={36 + halfStrokeWidth} cy={18 + halfStrokeWidth} r={processedRadius} />
</g>
</svg>
</div>
);
}
animate-spin 속성을 사용해 브라우저가 로드되지 않았을 때에도 회전 애니메이션이 잘 작동하도록 구현했습니다. 또한 react-loader-spinner처럼 수정 가능한 다양한 옵션을 위해 className, width, height, color, visible, strokeWidth, radius 를 제공하여 사용 환경에 따라 적절히 형태를 변경할 수 있도록 했습니다.