- Published on
리액트 서버사이드 렌더링과 컴포넌트
- Author
- Name
- yceffort
next + react 로 서버사이드 렌더링 환경을 구축하면서 개발을 하고 있었는데 두 가지 문제에 부딪혔었다.
window is not defined
, SSR 환경에서의 컴포넌트
1. 먼저 원래 코드를 보자.
import Calendar from '@toast-ui/react-calendar'
export default function Index() {
return (
<Calendar
view="month"
month={{
narrowWeekend: true,
}}
onBeforeCreateSchedule={(e) => {
setOpenCreatePopup(true)
setSelectedDate(e.start.toDate())
}}
onClickSchedule={(e) => {
console.log(e)
}}
scheduleView
calendars={calendars}
schedules={schedules}
/>
)
}
Server Error
ReferenceError: window is not defined
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Call Stack
Object.<anonymous>
file:///.../node_modules/tui-calendar/dist/tui-calendar.js (16:4)
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("tui-code-snippet"), require("tui-date-picker"));
else if(typeof define === 'function' && define.amd)
define(["tui-code-snippet", "tui-date-picker"], factory);
else if(typeof exports === 'object')
exports["Calendar"] = factory(require("tui-code-snippet"), require("tui-date-picker"));
else
root["tui"] = root["tui"] || {}, root["tui"]["Calendar"] = factory((root["tui"] && root["tui"]["util"]), (root["tui"] && root["tui"]["DatePicker"]));
})(window, function(__WEBPACK_EXTERNAL_MODULE_tui_code_snippet__, __WEBPACK_EXTERNAL_MODULE_tui_date_picker__) // 여기에서 에러가 난다.
해당 컴포넌트는 최초 시작시에 window
가 필요한데, 서버사이드 렌더링 시에는 window
가 없는 환경이기 때문에 에러가 난다.
아래 코드를 넣고, 최초 페이지 접근시에 새로고침을 하면 이 모듈이 실행되는 환경이 node 임을 알 수 있다.
console.log('node >> ', globalThis === global) // true
결론적으로 이 컴포넌트는 서버사이드 렌더링을 지원하지 않고 있으며, 이를 해결하기 위해서는 window
가 있는 브라우저 환경에서만 import 해서 사용해야 한다. 이를 nextjs에서 처리하기 위해서는 아래와 같이 하면 된다.
// dynamic 만으로는 부족하다. 꼭 ssr을 꺼야 한다.
import dynamic from 'next/dynamic'
const Calendar = dynamic(() => import('@toast-ui/react-calendar'), {
ssr: false,
})
2. Next SSR 환경에서의 ref
export default function Index() {
const cal = useRef()
useEffect(() => {
console.log(cal.current)
}, [cal])
return <Calendar ref={cal} />
}
위의 log 는 아래와 같이 찍힌다.
{retry: ƒ}
retry: ƒ ()arguments: (...)caller: (...)length: 0name: "bound retry"__proto__: ƒ ()[[TargetFunction]]: ƒ retry()[[BoundThis]]: LoadableSubscription[[BoundArgs]]: Array(0)__proto__: Object
useEffect
는 SSR에서 절대로 실행되지 않는다. 이를 해결하기 위해 useServerEffect라고 불리우는(?) 해괴한 effect가 있지만 , 굳이 그럴필요 없이 next의getServerSideProps
를 사용하면 된다.
왜 useRef
는 정상적으로 동작하지 않는 것일까?
useRef
returns a mutable ref object whose.current
property is initialized to the passed argument (initialValue). The returned object will persist for the full lifetime of the component....
This works because useRef() creates a plain JavaScript object. The only difference between
useRef()
and creating a{current: ...}
object yourself is that useRef will give you the same ref object on every render.
https://reactjs.org/docs/hooks-reference.html#useref
useRef
는 순수한 자바스크립트 객체이며, 컴포넌트가 아무리 렌더링이 된다고 해도 같은 ref
객체를 반환한다. 그런데 현재 ref.current
에는 retry
만 존재한다. 이것은 무엇일까?
위 두 코드에 정답이 나와있다. useImperativeHandle
를 통해서 ref
를 노출하고 있기 때문에, current에는 현재 세팅되어 있는 retry
만 보이고 있었던 것이다. useImperativeHandle
는 forwardRef
와 사용해야 한다.
useImperativeHandle
customizes the instance value that is exposed to parent components when usingref
. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used withforwardRef
:
https://reactjs.org/docs/react-api.html#reactforwardref
forwardRef
는 전달 받은 ref
속성을 하부트리의 다른 컴포넌트로 전달 할 수 있는 리액트 컴포넌트를 생성한다.
// #components/TuiCalendarWrapper
import React from 'react'
import Calendar from '@toast-ui/react-calendar'
export default (props) => (
// 3. 넘겨받은 `forwardedRef`를 진짜 컴포넌트에 넘긴다.
<Calendar {...props} ref={props.forwardedRef} />
)
const TuiCalendar = dynamic(() => import('#components/TuiCalendarWrapper'), {
ssr: false,
})
// 2. forwardRef를 통해서 전달받은 ref를 하위 컴포넌트에 보낸다.
const CalendarWithForwardedRef = React.forwardRef((props, ref) => (
<TuiCalendar {...props} forwardedRef={ref} />
))
export default function Index() {
const ref = useRef()
// 1. ref를 넘겨준다.
return <CalendarWithForwardedRef ref={ref} />
}
Ref를 포워딩 하는 방법은 여기에 더 자세히 나와있다.