Nextjs 13, Swiper Styling Not Working - hydration

CGH96·2023년 5월 22일
3

Nextjs

목록 보기
2/4

# 서론

Nextjs를 사용한 프로젝트에 Carousel을 구현하기 위해 외부 라이브러리인 Swiper를 사용 중이었다. 하지만 어째서인지 html tag들은 렌더링이 되었으나, fast refresh를 하기 전까지 Swiper의 js모듈이 적용되지 않았다. 이로 인해 Custom Navigation Button이 적용되지 않는 문제, 렌더링 초기에 반응형 디자인이 적용되지 않는 문제가 발생했다.


# 발생한 문제

1. Swiper Carousel에 Responsive Design이 적용되지 않은 모습이 초기에 보이는 문제

2. Swiper의 Custom Navigation Button이 작동하지 않는 문제



# 원인 - Hydration

위에 발생한 두가지 문제 모두 hydration과 관련된 문제였다. 아무 에러 메세지도 발생하지 않아서 원인을 파악하는데 애를 먹었다.
그렇다면 hydration은 무엇일까? 간단하게 알아보자.


Nextjs 13에서 client component의 경우, 서버사이드에서 html이 pre-render가 되고, 클라이언트 사이드에서 hydrate된다.
좀 더 쉽게 얘기하자면 다음과 같은 순서로 작동한다.

  1. 서버사이드에서 정적인 html을 미리 생성하여 렌더한다.

  2. pre-render한 html을 클라이언트 사이드로 보낸다.

  3. 번들링된 js를 클라이언트 사이드로 보낸다

  4. DOM에 eventListener들을 정해진 자리에 붙여 넣는다.

  5. 눈으로 볼 수만 있던 페이지가 interactive하게 변한다.

javaScript를 html에 채워넣는 모습이 마치 수분을 채워넣는 모습 같아서 hydration이라 부른다고 한다. hydration의 뜻은 수화시키다, 수분을 유지하다인데 수분을 직접 채워넣는 뉘앙스다. 여기서는 js가 물이 되는 것이다.

hydration과 관련된 이슈임에도 아무런 에러메세지가 발생하지 않은 이유를 여기서 추측할 수 있다.
HTML의 모습이 Server와 브라우저에서 모두 같은 모습이었기에 아무런 에러메세지도 발생하지 않은 것이다.

Server에서 렌더링된 HTML과 Browser에서 초기에 렌더링된 HTML의 모양이 다르다면 EventHandler를 붙이는데 문제가 발생한다.
Server에서 렌더링 되는 시점에 js번들을 어느 위치에 붙여야 할지 미리 정하게 되는데, 브라우저에 도착해서 EventHandler를 붙이려고 보니 HTML의 모양이 다르다면 적절한 곳에 붙일 수 없기 때문이다.

hydration을 간접적으로 확인 할 수 있는 방법이 뭐가 있을까 하다가 네트워크 탭을 확인해보았다. 초기에 localhost(document)가 로드되고 추후에 js가 로드 되는 것을 확인 할 수 있다.



이제 내 코드를 보자.

breakpoints를 통해 Responsive Carousel를 구현하려 했고, Swiper외부에 Navigation button을 만들어 useRef를 통해 참조하여 Swiper에 붙여주려 하였다.

hydration과 현재 발생한 문제를 고려할 때 다음과 같은 흐름으로 문제가 발생했음을 추측할 수 있다.

  1. 서버사이드에서 Swiper Component에 props로 전달한 js코드들이 제외되고 정적인 HTML만 렌더링.

  2. 브라우저로 HTML 로드

  3. js번들 로드. 하지만 Swiper Component의 props는 이미 js가 없을 때 아무값도 들어가지 않은 채로 초기화 및 렌더링 되었고, props에 값을 전달 할 수 없다.

내가 아는 hydration이라면 서버에서 Swiper의 정적 HTML이 렌더링 되고 js코드들이 Swiper에 맞게 전달 되어야 하는데 그렇지 못한 것이다.

pages router기반에선 관련 이슈가 없는 것 같은데 app router에서는 유명한 이슈인 듯하다. 아무래도 app router가 나온지 얼마 되지 않았고, third party라이브러리들이 이에 맞게 업데이트 되지 못하면서 발생하는 문제인 것 같다.

그렇다면 어떻게 해야할까??

Swiper과 관련된 HTML들이 브라우저로 넘어와서 렌더링되게 해주고, navigation 버튼 또한 브라우저에서 렌더링 된 이후에 연결시켜주면 되는 것이다.

# 해결 방안

1. Responsive Design 초기에 적용안되는 문제

loading state를 만들어주어, js로드가 끝나면 렌더링 되도록 해주었다.

2. Custom Navigation Button이 작동 안하는 문제

navigation props에 useRef를 미리 넣어주는 것이 아니라, 클라이언트 사이드에서 js가 로드되고 useRef가 정의되면 navigation을 연결하도록 수정했다.



✨✨hydrateRoot

hydration을 위에서 간단하게 알아보았다.

하지만 React 18 버전으로 오면서 React Server Component라는 개념이 생겼고 Nextjs 13버전 부터 이것을 활용하고 있는데 이에 따라 바뀐 부분들을 조금 더 자세히 알아보고 싶어졌다.


React는 18버전이 되면서 최상단의 root Node를 업데이트 하는 방식이 수정되었고, 이에 따라 hydrate함수가 deprecated되고 hydrateRoot라는 함수가 생겼다.


1. ReactDOM.render vs ReactDOM.createRoot

우선 React17에서 18로 넘어오면서 바뀐 Root API에 대해 알아보자.


React 17버전까지는 ReactDOM.render()함수를 사용하여 page의 root element를 렌더링했다. 코드는 다음과 같다.

ReactDOM.render (legacy)


위 코드의 문제점은 SPA의 특성상 root Node는 바뀔 일이 없음에도 업데이트가 될 때마다 root Node를 render함수에 계속 새롭게 전달 해주어야 하는 것이다.


이것을 개선하기 위해 React18버전에서는 ReactDOM.render가 deprecated되고 ReactDOM.createRoot라는 함수가 새롭게 생겼다.

ReactDOM.createRoot (New)


다음과 같이 수정되면서 virtual DOM에서 업데이트가 발생할 때마다 container를 전달 해주어야 하는 issue가 해결 되었다. 그렇다면 createRoot는 어떤 식으로 DOM Update를 다룰까.


createRoot는 renderunmount함수를 return하고 render함수를 호출하면 DOM이 Update, unmount를 호출하면 DOM을 제거한다.



2. ReactDOM.hydrate vs ReactDOM.hydrateRoot

ReactDOM.render에서 ReactDOM.createRoot로 변화하면서 hydrate또한 변화가 있었다.
ReactDOM.hydrate함수가 deprecated되고 ReactDOM.hydrateRoot가 업데이트 되었다.

hydrateRoot 함수가 Nextjs13의 Client Component에 대해 hydration이 작동하도록 해준다.

hydrateRoot


hydrateRoot의 특징은 다음과 같다.
1. Server-Side rendered ContentClient-Side rendered Content를 비교한다. 서로 일치하지 않을 경우 경고 메세지를 준다.
2. hydrateRoot가 실행되기 전에 createRoot가 실행되어 HTML content가 렌더되어야 한다.
3. hydrateRoot는 React App 전체를 통틀어 거의 대부분 한번만 실행된다.

hydrate, hydrateRoot를 비교해보려 했으나, 동작 방식에서 큰 차이가 없는 것 같다.

hydrate(legacy) 공식문서 링크
hydrateRoot 공식문서 링크

0개의 댓글