Next.js & Editor , SSR, REF 이슈

broccoli·2021년 4월 14일
2

next@troubleshooting

목록 보기
1/4
post-thumbnail

window is not defined, document is not defined

quill과 tui을 사용해 봤는데, 두개 모두 브라우저의 객체를 참조하고 있다. 이런 라이브러리들의 경우 next.js의 ssr 옵션을 끄고 참조를 진행해야한다.

next.js는 디폴트로 각 페이지들은 빌드시 pre-render를 한다. 이때 실행환경은 node이므로 글로벌한 객체는 window도 아니고 브라우저에서 실행되지도 않으므로 document(window.document)도 당연히 아니다. 따라서 client-side에서만 사용되는 라이브러리의 코드들은 ssr 옵션을 끄고 동적으로 참조해야한다.

ssr 제거옵션

next.js는 with no ssr이라는 설정을 제공한다. 참조링크

import dynamic from 'next/dynamic'

//return promise
const DynamicComponent = dynamic(() =>
  import('../components/hello').then((mod) => mod.Hello),
  //ssr 옵션 끄기
  {ssr: false}
)

tui editor 추가 예

WrappedEditor.jsx

//path: components/WrappedEditor.jsx
import React from 'react'
import PropTypes from 'prop-types'
import { Editor } from '@toast-ui/react-editor'

const WrappedEditor = (props) => {
  const { forwardedRef } = props
  return <Editor {...props} ref={forwardedRef} />
}

WrappedEditor.propTypes = {
  forwardedRef: PropTypes.shape({
    current: PropTypes.instanceOf(Element)
  }).isRequired
}

export default WrappedEditor

Editor.jsx

//path: components/Editor.jsx
import React, { useRef, useCallback, useState, useEffect } from 'react'
import dynamic from 'next/dynamic'
import PropTypes from 'prop-types'

const Editor = dynamic(() => import('./WrappedEditor'), { ssr: false })

const EditorWithForwardedRef = React.forwardRef((props, ref) => (
  <Editor {...props} forwardedRef={ref} />
))

const Index = (props) => {
  const editorRef = useRef(null)
  const [isLoaded, setLoad] = useState(false)
  const onChange = useCallback(() => {
    if (!editorRef.current) return
    const instance = editorRef.current.getInstance()
    props.onChange(instance.getHtml(), instance.getMarkdown())
  }, [props, editorRef])
  useEffect(() => {
    if (editorRef.current) {
      setLoad(true)
    }
  }, [editorRef.current])
  useEffect(() => {
    if (isLoaded) {
      props.onLoad(editorRef.current)
    }
  }, [isLoaded])
  return (
    <EditorWithForwardedRef {...props} ref={editorRef} onChange={onChange} />
  )
}

Index.propTypes = {
  onChange: PropTypes.func.isRequired,
  onLoad: PropTypes.func.isRequired
}

export default Index

ℹ️ Editor의 initialValue에 바인딩되어있는 값이 변경되어도 Editor에 반영이 안되서 onLoad이벤트 핸들러를 props로 보내 instance가 생성되었을 때 호출해주어 실제 Editor를 주입하고 있는 쪽에서 instancesetHtml, setMarkdown을 이용해서 수정해줌.

index.jsx

//path: pages/index.jsx
import React, { useCallback, useState, useRef, useEffect } from 'react'
import Editor from './libs/Editor'
import 'codemirror/lib/codemirror.css'
import '@toast-ui/editor/dist/toastui-editor.css'
import '@toast-ui/editor/dist/toastui-editor-viewer.css'

const Home = ({post}) => {
  const { markdown, content, title } = post
  const [titleValue, setTitleValue] = useState(title)
  const [htmlValue, setHtml] = useState(content)
  const [markdownValue, setMarkdown] = useState(markdown)
  const ref = useRef()
  const onChangeTitle = useCallback((e) => {
    setTitleValue(() => e.target.value)
  }, [])
  const onChangeEditValue = useCallback((htmlVal, mdVal) => {
    setHtml(() => htmlVal)
    setMarkdown(() => mdVal)
  }, [])
  const onLoad = useCallback((instance) => {
    ref.current = instance
  }, [])
  useEffect(() => {
    if (ref.current) {
      const instance = ref.current.getInstance()
      setTitleValue(() => title)
      instance.setMarkdown(markdown)
      instance.setHtml(content)
    }
  }, [post])
  return (
    <Container>
      <Editor
        placeholder="당신의 이야기를 적어보세요..."
        initialValue={markdownValue}
        previewStyle="vertical"
        initialEditType="markdown"
        height="100%"
        usageStatistics={false}
        useCommandShortcut
        onChange={onChangeEditValue}
        onLoad={onLoad}
      />
    </Container>
  )
}

export default Home

useRef

동적으로 임포트한 패키지의 경우 ref가 제대로 동작하지 않는다. 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.

useRef는 mutable ref object를 리턴값으로 한다. .current 속성은 전달받은 초기값으로 초기설정되고 이 리턴값은 컴포넌트가 유지되는동안 지속된다.

일반적으로 사용하는 경우는 자식컴포넌트에 명령적(?그냥 필수적으로 긴급적으로 불가피하게라고 이해하려한다.) 으로 접근하는 경우

function TextInputWithFocusButton() {
  const inputEl = useRef(null);
  const onButtonClick = () => {
    // `current` points to the mounted text input element
    inputEl.current.focus();
  };
  return (
    <>
      <input ref={inputEl} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}

쉽게 이야기하면 useRef는 그냥 하나의 블랙박스 같은 놈인데 이놈은 mutable object를 .current에 컴포넌트 라이프사이클동안 가지고있는 놈이라는 것이다.

어쩌면 access the DOM하는 방식으로 refs에 더 익숙할 수 있다. 만약 ref object를 React에 <div ref={myRef} />이렇게 전달하면, React는 이 노드가 변경될 때마다 거기에 상응하는 DOM node에 .current 을 설정할 거다.

useRef()ref 속성보다 훨씬 유용하다. 왜냐면 클래스에서 인스턴스변수값을 사용하는 것과 유사한 방식으로 mutable value를 유지할 수 있기 때문이다.

이런 유사한 방식을 사용할 수 있는 이유는 useRef가 단순히 js object를 생성해주기 때문인데, useRef()를 사용하는 것과 {current:...}를 생성하는것의 유일한 차이는 useRef()를 이용해서 생성한것은 렌더될 때마다 항상 동일한 참조값을 유지할 수 있다는 점이다.

따라서 반드시 알고 있어야하는 점이 있다.

useRef는 content가 변경될 때 noti 해주지 않는다. .current 속성이 변경되더라도 re-render를 야기시키지 않는다는 말이다. 만약 DOM 노드에 ref를 연결하거나 분리할때 어떤 코드가 실행되길 원한다면 callback ref(useCallback)를 대신 사용해라.

React.forwardRef

React.forwardRef 는 전달받은 ref 속성을 하부 트리내 다른 컴포넌트로 전달하는 React 컴포넌트를 생성한다. 이방식은 자주 사용되지 않으나 아래 두 경우에 유용하다.

  • Forwarding refs to DOM components
  • Forwarding refs in higher-order-components

React.forwardRef는 rendering 함수를 파라미터롤 받는다. 그리고 React는 이 함수를 propsref 파라미터를 받고 호출한다. 이 함수는 React node를 리턴해야한다.

const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;

위 예제에서 React는 <FancyButton ref={ref} />에서 받은 ref를 전달하고 이건 React.forwardRef 호출시에 렌더링함수에 2번째 인자로 전달한다. 그리고 이 렌더링 함수는 ref<button ref={ref} /> 에 전달한다.

따라서 ref.current<button /> DOM 엘리먼트 인스턴스를 직접 가리키게 된다.

ℹ️ 참조: https://yceffort.kr/2021/03/server-side-rendering-and-react-components
ℹ️ 참조 : https://myeongjae.kim/blog/2020/04/05/tui-editor-with-nextjs

profile
🌃브로콜리한 개발자🌟

0개의 댓글