SVG Sprite Icon 사용시 발생한 Hydration Error 처리

coggiee
7 min readJul 1, 2024

--

모각GO 프로젝트를 진행 중에 SVG Sprite방식의 Icon을 사용하게 되었고, hydration error를 마주했다.

문제 정의

Sprite로 구현한 Icon 컴포넌트(SVGProvider)에서 Hydration 오류가 발생했다.
Error: Hydration failed because the initial UI does not match what was rendered on the server. Warning: Expected server HTML to contain a matching <svg> in <body>.

문제 상황

  • SVGProvider.tsx(client) → 사용하는 아이콘 svg 코드를 모두 가지고 있는 곳이며 createPortalbody에 추가(렌더링)된다.
  • Icon.tsx(client) → props로 전달받은 iduse로 불러와서 실제 아이콘 컴포넌트를 사용하는 곳이다.

SVGProvider.tsx의 코드는 아래와 같다.

먼저 hydration error는 기본적으로 SSR 환경에서 발생한다.
이는, SSR에서의 HTML과 CSR에서의 HTML의 형태가 달라서 발생하는 오류이다.

NextJS의 경우 SSR → CSR 순서로 렌더링을 수행하는데, 클라이언트 컴포넌트여도 SSR에서 일단 실행(렌더링)된다.
그래서, use client로 선언되었더라도 초기에 SSR에서 한 번 실행이 되고 그 후 CSR로 한 번 더 실행되는 구조를 가진다.

hydration error를 어떻게 처리할까

코드를 보면, SVGProvider는 createPortal(spriteSVGCode, document.body)를 반환한다.
이때 document는 DOM이 생성된 이후에 접근할 수 있다.
초기에 SSR을 수행할 때는 DOM이 생성되지 않은 시점 이기에 document에 접근할 수가 없어서, hydration error가 발생하는 것이다.

그래서, DOM이 생성 됐을때를 판별해서, createPortal을 수행해야 한다.
일반적으로, DOM이 생성됐는지를 확인하기 위해서는 window 객체가 존재하는지 판별하는 코드를 사용한다.

window 객체는 DOM이 생성된 이후에 접근할 수 있다.

내가 생각한 로직은 아래와 같다.

  1. 마운트가 됐는지(DOM이 생성됐는지) 여부를 확인하기 위한 state를 생성한다.
  2. useEffect로 DOM에 접근가능한지 확인한다.
  3. 반환값을 반환한다.
function SVGProvider() {
const [isMounted, setIsMounted] = useState(false); // DOM 생성 여부
	useEffect(() => {
// if (typeof window === 'undefined') return; useEffect는 DOM 생성 이후에 실행됨이 보장되므로, window 객체 존재 유무를 판별하지 않아도 된다.
setIsMounted(true); // DOM 생성됨
}, []);

return isMounted && createPortal(spriteSVGCode, document.body); // DOM이 생성됐을 때만, createPortal을 수행한다.
}

처음에는 useEffect 내부에서 window 객체의 존재 유무를 판단했었는데, 이는 반은 맞고 반은 틀린 코드이다.일
일단, useEffect 자체는 클라이언트 단에서 실행됨이 보장되기 때문에, useEffect 내부 코드가 실행됐다는 것은 이미 DOM이 생성됐음을 의미한다.
즉, window 객체는 이미 접근이 가능하므로 존재 유무를 판별할 필요가 없다.

더 나아가서

hydration error는 SVGProvider 뿐만 아니라 생각보다 여러 군데에서 발생했다.
그렇기에 중복 로직을 최소화할 필요성이 대두되었다.

중복을 최소화시킬 방법은 2가지를 생각했다.

  1. custom hook
  2. HOC

둘 중 어느 것을 사용해도 상관은 없지만, Custom Hook을 사용한다면, 따로 호출부에 선언을 해야하고 반환값으로 분기 제어해야 하는게 번거롭다고 느껴져서 HOC를 선택했다.
(=Custom Hook의 경우 마운트 여부에 따라 분기처리를 해야 하지만 HOC는 코드 내부에서 한 번에 분기 처리까지 수행한다.)
+) 그리고 HOC를 한 번도 사용해본 적이 없어서 경험을 해보고 싶었던 이유도 있다.

Custom Hook으로 정의

// Custom Hook 정의
function useHydrationErrorBoundary() {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted;
}
export default useHydrationErrorBoundary;
// 실제로 사용
function MyComponent() {
const isMounted = useHydrationErrorBoundary();
if (!isMounted) return null; // 마운트 되기 전에는 null 반환
return (
<div>
{/* 컴포넌트의 내용 */}
<h1>Hello, World!</h1>
</div>
);
}
export default MyComponent;

HOC로 정의

function WithOnMounted<P extends object>(Component: ComponentType<P>) {
return function Mount(props: P) {
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
}, []);
return isMounted && <Component {...props} />;
};
}
export default WithOnMounted;
// 실제로 사용
function MyComponent() {
return (
<div>
{/* 컴포넌트의 내용 */}
<h1>Hello, World!</h1>
</div>
);
}
// HOC를 사용하여 MyComponent를 감싸서 export
export default withHydrationErrorBoundary(MyComponent);

HOC보다는 Custom Hook?

일단 HOC는 더 이상 사용하지 않을 것 같다. HOC를 사용하고 몇 가지 느낀 점이 있다면...

  1. Nested(중첩) 구조라면 사용이 복잡해진다.
  • 실제로, navigation render 유무를 판별하기 위해 인자를 요구하는 HOC와 동시에 hydration error를 처리하기 위한 HOC를 같이 사용해야 했는데 인자 전달 때문에 꽤나 까다로웠다.
  1. 멘토님도 의견을 주셨는데, HOC 같은 경우 컴포넌트의 계층이 깊어져서 디버깅을 어렵게 한다고 한다.
  2. 주관적이지만, 생성하는(만드는)게 복잡하다.

위와 같은 이유로 인해서, 앞으로는 HOC를 사용하지 않을 것 같다. 굳이? 라는 생각이 든다.
그리고 Custom Hook으로도 재사용성을 충분히 처리할 수 있기 때문에 앞으로는 Custom Hook을 사용할 것 같다.

앞서 Custom Hook을 선택하지 않은 이유로, 추가적인 분기 처리 제어를 꼽았는데 다시 생각해보면 오히려 명시적으로 흐름을 보여주는 것 같아 오히려 좋은 것 같기도 하다.

결과적으로 나의 취향은 Custom Hook에 더 가까운 걸 알게 됐고, HOC도 경험해볼 수 있어서 좋았다.

참고

https://velog.io/@yijaee/serverside-html-matching

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

coggiee
coggiee

Written by coggiee

0 Followers

Just doing

No responses yet

Write a response