quill과 tui을 사용해 봤는데, 두개 모두 브라우저의 객체를 참조하고 있다. 이런 라이브러리들의 경우 next.js의 ssr 옵션을 끄고 참조를 진행해야한다.
next.js는 디폴트로 각 페이지들은 빌드시 pre-render를 한다. 이때 실행환경은 node이므로 글로벌한 객체는 window도 아니고 브라우저에서 실행되지도 않으므로 document(window.document)도 당연히 아니다. 따라서 client-side에서만 사용되는 라이브러리의 코드들은 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}
)
//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
//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를 주입하고 있는 쪽에서 instance
의 setHtml
, setMarkdown
을 이용해서 수정해줌.
//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
동적으로 임포트한 패키지의 경우 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
는 전달받은 ref 속성을 하부 트리내 다른 컴포넌트로 전달하는 React 컴포넌트를 생성한다. 이방식은 자주 사용되지 않으나 아래 두 경우에 유용하다.
React.forwardRef
는 rendering 함수를 파라미터롤 받는다. 그리고 React는 이 함수를 props
와 ref
파라미터를 받고 호출한다. 이 함수는 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