(Next.js) 에러로그 : window is not defined

호두파파·2022년 4월 14일
7

Next.js

목록 보기
1/6


최근 합류한 팀에서 첫 업무로 리액트 프로젝트를 Next.js 프로젝트로 이식하는 작업을 맡게 되었다.

최대한 업무의 공수를 줄일 수 있는 마이그레이션(정작 시도를 해보면, 쉽지 않다는 것을 알 수 있다.)과 새롭게 프로젝트를 설치하고 시작하는 인스텔레이션 두 방향으로 고민을 했다.

잠깐의 고민 끝에 프로젝트의 구조가 간단하지 않아 구조를 파악할 수 있는 후자가 시간은 더 걸리겠지만, 사용하지 않는 라이브러리나 변수를 제거하고 스토리보드 라이브러리도 이식할 수 있겠다 싶어서 후자의 방향으로 프로젝트를 시작하게 되었다.

Next.js는 리액트 라이브러리(라지만, 프레임워크라고 설명하는 분들도 많다. 하지만, 나는 라이브러리라고 생각한다)와는 달리 프레임워크다.

따라서, 반드시 지켜야 할 룰들이 있다보니 프로젝트의 구조를 전체적으로 이해하기 위해 노마드 코더 니꼬쌤의 강의를 수강하고 전체적인 진행방향을 이해하고 프로젝트를 진행했다.

이 글은 프로젝트 초반부인 현재 가장 큰 난제였던 문제에 대해 다룬 글이다.
아마, Next.js를 처음 다루는 누군가에게는 반드시 도움이 될 것 같다.


😱 window is not defined

Next.js는 리액트 프레임워크이고 프리랜더링을 지원한다. 프리랜더링을 위해 Next.js는 페이지를 랜더링할 때, SEO 최적화와 성능 개선을 위해 HTML을 먼저 생성한다는 이야기가 된다.

서버사이드 랜더링의 특징을 차용하고 있기 때문에, window를 참조하는 이벤트를 실행하는 코드를 사용하면 다음과 같은 에러를 만날 수 있다.

// components/Scroll.js
window.addEventListener("scroll", function() {
  console.log("scroll!")
});


Next.js는 기본적으로 Node.js 환경에서 동작한다.
따라서, window 객체는 아직 정의되어 있는 것이 아니다. window는 오직 브라우저에서 참조가능한 것이기 때문이다. 이점에서 클라이언트 사이드 렌더링으로 진행되는 리액트 앱과 서버 사이드 렌더링 형식을 지원하는 Next.js의 차이점이 분명하게 드러난다.

window를 쓰려면 process.browser로 브라우저 상태인지 확인하는 등의 참고 작업이 필요하다.

이 점을 해결하기 위해 다양한 포스팅을 참조했다. 공통적으로 제시하는 해결방법은 다음과 같다.


1. 첫 번째 해결책 : type of 분기처리 해주기

if (typeof window !== "undefined") {
  // Client-side-only code
}
// or
if (typeof window === 'undefined') return;

위 방법으로 클라이언트에서 렌더링할 때까지 기다렸다가, window를 참조할 수 있는 시점에 블락 안에 있는 코드를 실행하는 방식이다.

2. 두 번째 해결책 : useEffect 안에서 코드를 사용하기

// components/Scroll.js
import React, { useEffect } from "react";

 const Scroll = () => {
  useEffect(function mount() {
    function onScroll() {
      console.log("scroll!");
    }
    window.addEventListener("scroll", onScroll);
    return function unMount() {
      window.removeEventListener("scroll", onScroll);
    };
  });
  return null;
}
export default Scroll
// pages/index.js

import Scroll from "../components/Scroll";

export default function Home() {
  return (
    <div style={{ minHeight: "1000px" }}>
      <h1>Home</h1>
      <Scroll />
    </div>
  );
}

useEffect 훅을 사용해서 브라우저에 렌더링 되는 시점에 코드를 실행시키는 것이다.
클라이언트 사이드에서 실행되어야 할 코드를 useEffect안에 고립시키고, 서버사이드에서 실행되어야 할 코드를 getServerSideProps단에 분리시키는 것이다.

const MyPage = () => {
  useEffect(() => {
    // client side stuff
    // window is accessible here.
  }, [])

  return (
    <div> ... </div>
  )
}

MyPage.getServerSideProps = async () => {
  // server side stuff
}

3. 세 번째 해결책 : Dynamic 사용하기

공식문서에서 제안하는 방법 살펴보기

// components/Scroll.js

function onScroll() {
  console.log("scroll!");
}

window.addEventListener("scroll", onScroll);

export default function Scroll() {
  return null;
}
// pages/index.js

import dynamic from "next/dynamic";

const Scroll = dynamic(
  () => {
    return import("../components/Scroll");
  },
  { ssr: false }
);

export default function Home() {
  return (
    <div style={{ minHeight: "1000px" }}>
      <h1>Home</h1>
      <Scroll />
    </div>
  );
}

서버 사이드 랜더링 단에서 false 옵션을 주어, 클라이언트에서 랜더링될때 해당 컴포넌트가 마운트되고, 그 시점에서 코드가 실행되는 것이다. 2번째 방법과 유사한 방법으로 useEffect를 사용하지 않고도 구현가능한 방법이다.


JSencrypt 라이브러리를 사용했을 때 해결책

위에서 제기한 방법들은 공통적으로 서버 사이드 랜더링 단에서 코드가 실행되지 않도록 옵션을 두는 것이다. 하지만, 내 경우에는 커스텀 훅에서 사용하고 있는 JSencrypt라이브러리에서 에러가 발생하고 있어서 3가지 방법 중 어느 하나를 마땅하게 적용하기 어려웠다.

팀에서 컨플루언스에 에러로그를 작성하고 있는데, 다행히 컨플루언스에 팀장님께서 해결방법을 기록해두셨다. (😃 역시 기록은 생산을 낳는다더니..!)

원래 코드

import { JSEncrypt } from 'jsencrypt
const encrypt = new JSEncrypt()
const encryptedPassword = encrypt.encrypt(password)

import를 사용해서 해결하기

const JSEncrypt = (await import("jsencrypt")).default
const encrypt = new JSEncrypt()
const encryptedPassword = encrypt.encrypt(password)

라이브러리가 임포트 될때까지 기다렸다가, 임포트되면 JSencrypy객체의 default값을 참고하는 것이다. 이 점에서 3번째 방식으로 제시된 dynamic import와 유사하다고 생각했다.


정리

Next.js의 캐릭터를 제대로 파악할 수 있는 문제였다고 생각한다. 물론, 검색 결과를 도입해서 해결하지는 못했지만, 팀 프로젝트의 이슈를 공유해야할 필요성에 대해서 몸으로 체감할 수 있는 기회익시도 했다.

Jsencrypt라이브러리를 사용하는 누군가.. next.js를 마이그레이션하는 업무를 맡게된다면 도움이 되었으면 하는 마음을 담아 글을 남긴다.

참고

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

0개의 댓글