본문으로 건너뛰기
yceffort
PostsSeriesTagsAbout🧪 Research
EN

Tweaks

theme
accent palette
film grain
minimal mode
BACK TO INDEX
◆ ESSAY
--min
--year
KOoriginal

mailMail icongithubtwitter
yceffort
•
© 2026
•
https://yceffort.kr
BACK TO INDEX
◆ ESSAY

리액트 서버사이드 렌더링과 컴포넌트

avatar
yceffort
2021-03-19 · 4분
4min
2021year
KOoriginal
nextjsreactssr

next + react 로 서버사이드 렌더링 환경을 구축하면서 개발을 하고 있었는데 두 가지 문제에 부딪혔었다.

1. window is not defined, SSR 환경에서의 컴포넌트

먼저 원래 코드를 보자.

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만 존재한다. 이것은 무엇일까?

https://github.com/vercel/next.js/blob/f06c58911515d980e25c33874c5f18ade5ac99df/packages/next/next-server/lib/loadable.js#L219-L260

https://github.com/vercel/next.js/blob/f06c58911515d980e25c33874c5f18ade5ac99df/packages/next/next-server/lib/loadable.js#L161-L173

위 두 코드에 정답이 나와있다. useImperativeHandle를 통해서 ref를 노출하고 있기 때문에, current에는 현재 세팅되어 있는 retry만 보이고 있었던 것이다. useImperativeHandle는 forwardRef와 사용해야 한다.

useImperativeHandle customizes the instance value that is exposed to parent components when using ref. As always, imperative code using refs should be avoided in most cases. useImperativeHandle should be used with forwardRef:

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를 포워딩 하는 방법은 여기에 더 자세히 나와있다.

https://reactjs.org/docs/forwarding-refs.html

관련 글

  • ◆ Next.js의 현주소
    #nextjs#web-performance#react

    Next.js의 성능은 충분히 빠른가

    벤치마크가 말해주는 불편한 진실

    2026-03-21·42분
  • ◆ 디렉티브 딥다이브
    #react#nextjs#frontend

    'use cache' 디렉티브 딥다이브: 캐시 경계의 끝까지

    "use cache" 한 줄이 만드는 빌드 타임 변환, 캐시 키 직렬화, ResumeDataCache, cacheHandler, 그리고 Cache Components까지

    2026-05-01·65분
  • ◆ 디렉티브 딥다이브
    #react#nextjs#frontend

    'use client' 디렉티브 딥다이브: 클라이언트 경계의 끝까지

    "use client" 한 줄이 만드는 모듈 경계, 빌드 타임 변환, Flight 직렬화, 그리고 성능까지

    2026-05-01·52분
  • ◆ Next.js의 현주소
    #nextjs#react#vercel

    왜 여전히 Next.js를 쓰는가

    기술 우위보다 강한 전환 비용

    2026-03-23·35분

새 글을 놓치고 싶지 않으시다면 RSS로 구독해 주세요.

RSS 구독 →

yceffort — 프론트엔드 엔지니어입니다. 발표·기술 자문·기고 문의는 이곳에서 받고 있습니다.

← Back to the blogIssue on GitHub →