ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [2020.01.05] Next.js에서 document가 undefined으로 나타나는 이유
    개발 블로깅/Next.js 2020. 1. 5. 15:39


    이번에 트레바리에서 각종 이벤트와 마케팅, 모임 안내 등 여러 수신 알림 채널을 계획 중이다.

    기존에는 이러한 정보를 수신받기 위한 채널이 문자전송 밖에 없었는데, 앞으로는 문자뿐 아니라 이메일과 다른 요소 등 전송 채널을 더 넓혀서 유저들이 누릴 수 있는 유용한 정보를 놓치지 않고 더 최대한 많은 혜택을 누릴 수 있도록 하기 위함이다.

    그래서 이러한 마케팅 정보 수신 안내를 기존 멤버들에게 알리고 동의를 받고자, '트레바리 마케팅 정보 통합 수신 동의' 기능을 구현하기로 했는데, 해당 기능을 구현 중 맞닥뜨린 이슈사항이 하나 있었었으니...바로 document가 undefined로 나타나는 것이었다..

    처음이 이러한 현상이 어떻게 일어날 수 있는지 몰랐으나, 알고보니 내가 CSR과 SSR의 동작원리에 대한 차이를 제대로 인지하지 못했기 때문이었다.

    그래서 이번 글을 통해 CSR과 SSR의 동작원리에 대해 한번 구체적으로 다뤄보고, 내가 겪었던 document가 undefined로 나타나는 이유에 대해 알아보자.

    Next.js는 무엇인가?

    우선 트레바리는 개발 프레임워크 환경으로 Next.js를 사용한다.
    Next.js는 리액트를 SSR(Server-Side rendering) 방식을 구현할 수 있도록 도와주는 프레임워크이다.
    정확하게는, SSR과 CSR의 각 장점을 조합하여 동작하게 된다.

    SSR과 CSR의 차이
    기본적으로 처음 웹 화면을 접속하면, DOM과 스크립트 코드, 리소스 등 해당 웹 페이지를 구성하기 위해 필요로 한 모든 요소들을 서버에서 로드하게 된다.

    SSR은 화면에서 보여줄 특정 페이지의 View를 서버 단에서 렌더링을 한 후, 클라이언트에게 제공된다. View를 빠르게 렌더링 후에 사용자에게 미리 웹 화면으로 보여주고, 이후에 웹 페이지를 동작하게 하거나 구성하는 리소스들을 로드하도록 한다. 그러면 사용자는 첫 화면을 보게 되면, 아직은 기능이 동작하지는 않지만 매우 빠른 로딩처럼 느낄 수가 있기 때문이다.

    대신 다른 페이지로 넘어갈 때는, 넘어가려는 페이지를 다시 요청, 렌더링을 해야 하므로 CSR보다 느리다는 단점이 있다.

    반대로 CSR은 모든 페이지의 View를 클라이언트로 로드 후에 렌더링을 하게 되므로, 로딩 시간이 SSR보다 느리다.
    하지만 다른 페이지로 넘어갈 때, 이미 모든 페이지가 렌더링이 되어 있기 때문에 SSR보다 빠르게 동작한다.
    Next.js의 렌더링 동작 원리
    SSR의 장점은 접속하려는 페이지의 View를 빠르게 렌더링하고 사용자에게 보여줌으로써, 처음 화면 로딩 시간이 빠르다는 것이다. 
    그래서 Next.js에서는 링크를 통한 접속이나 주소를 통한 접속은 SSR 방식을 이용하게 된다.
    CSR의 장점은 이미 클라이언트에서 전체 페이지가 렌더링이 되어 있어 페이지 전환이 빠르다. 그래서 이벤트를 통한 페이지 접속은 CSR 방식으로 일어나도록 되어있다.

    예를 들어, 사용자가 네이버 검색창에 '트레바리'를 검색하여 나타난 링크를 통해 트레바리 사이트에 처음 접속한다. 이때 서버는 사용자에게 빠른 웹 페이지를 보여주기 위해 서버 렌더링을 하게 된다.

    트레바리 메뉴 바에서 '마이페이지'를 클릭하여 마이페이지로 이동한다. 이때는 이벤트를 통한 페이지 전환이 빠르게 일어나기 위해 클라이언트 렌더링을 하게 된다.

     

    사실 SSR과 CSR의 차이에 대해서는 아주 예전부터 이미 여러 번 글을 읽어 봤었는데 이해가 제대로 되지 않은 상태로 넘어갔던 것 같다.
    그 당시에는 Next.js 코드 내부에서 getInitialProps를 이용하는 것을 제외하곤 개인적으로는 웹 페이지를 구현하는데 큰 차이를 느끼지 못했기 때문이다.

    그러나 이러한 SSR과 CSR의 근본적인 차이를 이해하지 못하여 발생한 문제가 있었고, 이를 파악하기에 꽤나 시간이 걸렸었다.

     

    문제가 발생한 기능 설명

     

    기존의 유저들에게 새로운 채널에 대한 마케팅 수신 동의를 받기 위해, 마이페이지 접속 시 모달 창을 이용해서 수신 동의 안내 창을 뜨도록 하는 기능을 만들었다. 

    해당 유저가 동의 안내에 대한 응답 내역이 없으면, Modal 컴포넌트의 isOpen props에 true를 미리 적용하여 페이지 접속을 하자마자 모달 창을 바로 띄우도록 할 예정이었다.

    그러나 기능을 실행시켰더니, 'document is not defined' 에러 메시지를 접하게 되었다.

     

    # 문제의 코드

    class Mypage extends React.Component<IProps, IState> {
      public static async getInitialProps({ res, req, asPath, query }: any) {
        
        //  SSR 처리가 일어나는 부분
    
        return {
          isModalOpen // 수신동의 안내 응답여부 확인 후 모달 창을 띄울지 여부 확인.  
        }             // SSR쪽에서 반환된 return 값은 렌더링 함수의 props로 전달한다.
      }
      
      render() {
        const {isModalOpen} = this.props; // SSR에서 넘겨받은 데이터
        
        return (
         <Modal isOpen={isModalOpen}> // Modal 컴포넌트의 isOpen이 true일 시, 에러 발생!
            ... 
         </Modal>
        )
      }
    }
    
    

     

    아니?! DOM을 그리는데 최상위 DOM인 document가 없다니..?!

     

    원인

    document가 정의되는 시점은 모든 CSR이 동작할 때이다. 더 구체적으로는 View 뿐 아니라 웹 페이지를 동작하는 요소들이 모두 클라이언트에 로드가 되었을 때를 말한다.

    이것을 토대로 생각해보면, 최상위 DOM인 document가 undefined가 나오기 위해서는 CSR이 동작할 때가 아닌, SSR동작에서 document를 이용하려 했을 때가 된다.
    즉, 아직 웹 페이지를 구성시킬 요소들이 렌더링 및 클라이언트로 로드되기 전에 document에 접근하여 View 요소를 조작하려하니 발생한 문제였던 것이다.

    💡렌더링 함수로 넘어가면 CSR 아닌가요?
    위의 SSR과 CSR의 렌더링 방식의 차이를 적은 것처럼, 주소 창이나 외부 링크를 통해 해당 페이지를 접속하게 되면 'getInitialProps' 함수 부분을 포함하여 렌더링 함수 또한 SSR 처리가 된다.
    즉 렌더링 함수가 같은 코드여도 해당 페이지에 어떻게 접속하느냐에 따라 서버 단에서 실행이 될 수도 있고, 클라이언트 단에서 실행이 될 수도 있다.

    다만 getInitialProps 함수는 SSR에서만 실행되는 서버 사이드 처리 함수이며, 렌더링이 동작하기 전에 실행된다.


    💡getInitialProps 함수는 어떤 상황에 쓰면 되는 것인가요?
    getInitialProps 함수를 이용하면 웹 페이지가 렌더링 되기 전에 서버 단에서 처리할 수 있는 작업들을 미리 처리한다면 시스템의 성능을 높일 수 있다. 
    예를 들어, 해당 페이지를 접속했을 때, 특정 조건을 만족하지 못하여 다른 페이지로 리다이렉트 처리를 해야 한다면, 굳이 현재 페이지의 렌더링 작업이 필요하지 않을 것이다. 그래서 서버단에서 리다이렉트를 처리하면 더욱 빠르게 다른 페이지로 이동할 수 있게 된다.

     

    View 렌더링 중, Modal에 세팅된 true 데이터로 인해 Modal이 띄어지도록 DOM 조작을 하기 위해 document에 접근했더니, 아직 서버 단에서 렌더링이 마치지 않아 클라이언트 로드가 되지 않아서 document가 정의되지 않은 것이었다.

    하지만 문제가 되는 위의 코드가 항상 에러가 발생하는 것은 아니다. 
    이는 SSR 방식에서나 나타나는 문제였고, CSR 방식으로 동작하게 되면 정상적으로 동작하게 된다.

    아래 에시를 봐보자.

    CSR 동작과 SSR 동작을 위한 사용자 액션의 예시

     

     

    메뉴에서 '마이페이지'를 클릭하여 해당 페이지로 이동하면 클라이언트 렌더링이 일어나게 된다. 이 때는 이미 모든 페이지 렌더링이 끝나고 클라이언트 단으로 페이지 로드가 된 상태이기 때문에 document가 선언이 되어있으므로 정상적으로 모달이 나타나게 된다.

    그러나 이 상태에서 새로고침을 하게 된다면, 서버 렌더링이 동작하게 되어 에러가 발생하게 될 것이고, 이는 서버 렌더링이 실행되면 다시 document가 선언되어 있지 않기 때문이란 것을 알 수 있다.

     

    '그러면 SSR 방식으로 페이지 접속 시, 무조건 DOM 접근으로 화면을 구성하는 방법은 불가능한가?'

    조금 더 구체적으로는 Next.js에서 SSR 방식으로 접속하면 DOM 조작을 할 수 없냐는 질문이 맞을 것 같다.
    이는 아래 설명을 통해서 알아보자.

    SSR 방식으로 페이지 접속 시 DOM을 접근하는 방법

    React에는 LifeSycle API가 있는 것을 알 것이다. 그중 ComponentDidMount를 이용하면 된다. ComponentDidMount는 해당 컴포넌트의 렌더링을 마치고 실행되는 함수이며, 클라이언트 단에서 실행이 되기 때문이다.

    # 문제를 해결한 코드

    class Mypage extends React.Component<IProps, IState> {
      public static async getInitialProps({ res, req, asPath, query }: any) {
        
        //  SSR 처리가 일어나는 부분
    
        return {
          // 렌더링 함수로 보낼 props 데이터 처리 부분
        }
      }
      
      constructor(props) {
        super(props);
        this.state = {
          isModalOpen:false  // 이전의 모달 창 실행 여부를 props에서 state로 변경.
        }
      }
      
      componentDidMount() {
        const isModalOpen = // 수신동의 안내 응답여부 확인 후 모달 창을 띄울지 여부 확인.
        this.setState({
          isModalOpen
        });
      }
      
      render() {
        const {isModalOpen} = this.state;
        
        return (
         <Modal isOpen={isModalOpen}> // 정상적으로 모달 창 실행.
            ... 
          </Modal>
        )
      }
    }
    
    

     

    위처럼 실행하면, SSR 방식으로 접근을 해도 모든 렌더링이 처리된 후 모달 창을 띄우기 위해 DOM 접근을 하게 되므로 정상적으로 기능이 동작하게 된다.


    문제를 해결하며 느꼈던 점과 깨달은 점

    사실 이번 문제를 해결하면서 내 스스로에 대해 엄청나게 많은 반성을 하게 되었다.

    내가 작업하고 있는 시스템 환경에 대해 정확하게 파악하고 있지 않은 상태로 개발을 하다보니 발생했던 문제였으며, 그로인해 정말 작은 문제이고 해결 방법 또한 간단했음에도 불구하고 원인을 파악하기까지 굉장히 많은 시간이 소요되었다.

    이번 문제를 해결하면서 내가 작업하는 시스템 환경과 아키텍처 뿐 아니라 내가 다루고 있는 것들은 앞으로 정확하게 알고 사용하자는 큰 깨달음을 얻을 수 있는 좋은 시간이었다.

    또한 이번 이슈 사항을 통해 CSR과 SSR의 동작원리에 대해 더 자세히 파악할 수 있었고, 향후에 렌더링으로 인해 발생하는 에러가 있다면 조금 더 수월하게 문제를 해결할 수 있을 것 같다.

    반응형

    댓글

Designed by Tistory.