[Trouble-Shooting] Recoil 새로고침 랜더링 문제

Joosi_Cool·2023년 8월 3일
6

Next.js

목록 보기
5/5
post-thumbnail

이번에 프로젝트에 Recoil 기술을 적용하면서 어떠한 문제에 직면했다.
이 부분에 대해 해결하는 과정을 공유하려 한다.

🥲 problem background

본인은 Next.js 프레임워크로 TypeScript 기반의 프로젝트에서 내비게이션 바를 만드는 작업을 했었다.

디자인은 직접적으로 보여주진 못하지만, 보통 어플리케이션에서 내비게이션 바는 모든 페이지에 들어간다. 따라서 layout.tsx에 관련 내비게이션 바를 넣는 방식을 취한다.

하지만 본인의 앱 UI에선 모든 페이지에 내비게이션 바가 있는 것이 아니라, 특정 페이지에서 나타났다.
이에 따라서 내비게이션 바 설계에 어려움을 겪었다.

그래서 본인이 생각한 건 다음과 같았다.

전역상태관리 라이브러리를 사용하여 내비게이션 바에 관련된 변수를 만들어서 필요할 때마다 불러와서 이를 통해 내비게이션 바를 랜더링 하자!

그래서 본인은 recoil 라이브러리를 사용하여 내비게이션 바에 대한 상태를 만들고, 내비게이션 바는 이를 랜더링 할때마다 불러와서 보여주는 방식을 채택하려 했다.

그러나 생각대로 되지 않았고, 문제가 발생했다.

위와 같은 상태임을 생각하고, 문제의 장면을 보자.

🤔 problem definition

뭐가 문제인지 알겠는가?
내비게이션 바에 값을 배열의 형태로 담아서 정의해두었다.
[Home, search, jokbo, My] 이런 형태의 boolean배열로 선언해두고, 활성화된 버튼은 true, 비활성화 버튼은 false가 되게 설계하였다.

그런데 여기서 Home을 이동하고, My로 이동하면 값이 잘 변화되어지는 것이 확인된다. 하지만 새로고침 시에 값이 default값인 [false, false, false, false] 가 출력된다.

본인이 쓴 코드를 봐보자.

  • NavigationAtom.tsx
"use client";
import { atom } from "recoil";

export const NavigationAtom = atom<boolean[]>({
  key: "NavigationAtom",
  default: [false, false, false, false],
});

위에서 말한것처럼 본인의 프로젝트에선 내비게이션 바가 모든 페이지에 존재하는 것이 아니다.

그래서 선택한 방법이 전역 변수 관리 라이브러리인 Recoil 을 사용하여 위 코드와 같이 내비게이션 바의 상태를 위와 같이 따로 저장해두었다가 필요할때마다 꺼내쓰는 방식으로 설계하였다.

그랬더니... 저렇게 발생하는 것이었다.

1. 문제 해결

✍ 문제 분석

새로고침 시에 값이 다 초기화되는 현상 발생

이를 공부해본 결과, 문제가 발생한 원인은 다음과 같다.
Recoil로 관리하는 상태는 클라이언트 사이드에서만 유지되므로, 페이지를 새로고침하면 서버 측 에서 초기 상태를 가져와서 다시 렌더링하기 때문에 Recoil 상태가 초기화된다.

쉽게 말해, 새로고침 시에 페이지가 랜더링 되면서 Recoil 상태도 같이 초기화 된다는 말이다. 다시 랜더링 하기 때문에 default값으로 지정해둔 값으로 초기화된다는 것을 의미한다..

🙌 해결 방법

이를 해결하기 위해서 생각한 방법은 localStorage또는 sessionStorage에 저장해두고, 새로고침 시에 이를 가져오면 되지 않을까? 라는 생각이 들었다....

많은 개발자들도 이 문제가 있었는지... 관련 라이브러리가 있다.
바로 Recoil-persist가 있다.
이는 localStorage또는 sessionStorage에 저장하여 새로고침에도 기존에 있던 값으로 덮어씌우는 역할을 한다.

  • Recoil-persist 의 이해

    이 작동 방법에 대해 간단히 보자면 Recoil값이 변화될 때 storage에 저장해두었다가 이를 가져오는 방식을 채택한다.
    이렇게 되면 새로고침을 할때 이 storage에 있는 값을 가져오는 방식으로 초기화 되는 현상을 방지할 수 있다.

그래서 본인의 코드를 다음과 같이 수정했다.

"use client";
import { atom } from "recoil";
import { recoilPersist } from "recoil-persist";

const sessionStorage =
  typeof window !== "undefined" ? window.sessionStorage : undefined;

const { persistAtom } = recoilPersist({
  key: "NavigationAtom",
  storage: sessionStorage,
});

export const NavigationAtom = atom<boolean[]>({
  key: "NavigationAtom",
  default: [false, false, false, false],
  effects_UNSTABLE: [persistAtom],
});

사용 방법에 경우, 아래 잘 정리된 블로그가 있어서 아래 공유하도록 하겠다.

[React] Recoil-persist 사용하여 localStorage와 sessionStorage에 저장하기

결과를 한번 봐보자....

😲 결과

일단 해결된 것은 아니다.
보면 새로고침 시에 원하던대로 내비게이션바가 작동하지 않는다.
그래서 저번처럼 똑같이 초기화 할때 atom도 같이 초기화됐나 싶었다.
하지만 콘솔 값을 보면 아래 에러가 발생하지만 값은 잘 불러오는 것을 확인할 수 있다.

하지만.. 내비게이션 바는 제대로 작동하지 않았다. 이유가 뭘까.

2. 문제 해결

✍ 문제 분석

우선 에러 메시지가 나온걸 봐보자.

여기에 대해 검색해보니, Next.js를 사용할 때 즉, SSR을 사용하는 경우에 storage의 변수를 이용해서 렌더링할 때 발생하는 경고라는 것을 알았다.

정리하면, 서버에서 렌더링된 결과와 클라이언트에서 렌더링된 결과가 일치하지 않아서 발생했다는 말이다.

본인이 위에 문제를 해결하기 위해, storage를 이용해서 값을 초기화하지 않게 하는 Recoil-persist 를 사용했다.
구동방식에 있어서 일단 처음엔 default값으로 페이지를 렌더링해서 받아온다.
이후에 Recoil-persist 을 통해 값을 가져오게 되면 랜더링하게 되는 값이 처음 랜더링 받아온 것이랑 다르게 된다.
무슨 말인지 이해가 갈 것이다.

정리하자면 처음 랜더링 될때는 default값으로 랜더링 되었다가, 나중에 Storage에 저장된 값을 불러오게 됨에 따라 처음 랜더링한 결과와 받아온 이후 결과가 달라진다는 말이다. 그래서 새로 고침 시에 default값으로 페이지는 랜더링 되지만, 값을 콘솔로 찍었을땐 잘 불러와진 것이다.

이 때문에 문제가 발생했다는 것으로 분석이 가능하다.

🙌 해결 방법

그렇다면 어떻게 해결하는 것으로 갈까..
우선 본인은 다음과 같이 생각했다. 값을 불러오기 전에 페이지를 먼저 렌더링해서 문제가 발생하는 것이지?? 그러면.... 값을 불러오고 원하는 것을 불러오면 되겠네.

이를 위해서 useEffect를 사용해서 페이지를 랜더링이 끝나면 그 내비게이션 바 부분을 불러와보자.

코친 코드를 봐보자.

// NavigationBar.tsx (내비게이션 바 컴포넌트)
"use client";
import { useRecoilState } from "recoil";
import { NavigationAtom } from "@/app/status/NavigationAtom";
import { useEffect, useState } from "react";

const NavigationBar: React.FC = () => {
  // 하단 바에 대한 state => true면 그 페이지로 이동
  // state = [홈, 검색, 족보, My] 에 대한 boolean 형태 배열
  const [navigationState, setNavigationState] = useRecoilState(NavigationAtom);
  const [loaded, setLoaded] = useState(false);
  
  //페이지 랜더링 되면 loaded값을 true로 변경 
  useEffect(() => {
    setLoaded(true);
  }, []);
  
  return (
    <>
      <div className="w-full h-auto flex justify-around border-t border-lightGray pt-[10px] fixed bottom-5">
        {loaded ? (
          <>
            {/*서버에서 렌더링된 결과와 클라이언트에서 렌더링된 결과를 일치 시켜야 하는 부분*/}
          </>
        ) : (
          <>loading</>
        )}
      </div>
    </>
  );
};

export default NavigationBar;

😲 결과

보시다시피 페이지 로딩이 끝나고 나서 값을 불러오게끔 하니깐 에러 메시지가 사라졌다. 그리고 내가 원하던대로 컴포넌트가 유지되는 것을 확인할 수 있다.


하지만 이는 보시다시피 loading 시간 이 필요하다. 매우 적절하지 않은 방식이다.

그래서 곰곰히 생각해본 결과, 내비게이션 바에서 Recoil같은 전역상태 관리 라이브러리를 사용하여 다루는 방식은 옳지 못하다.

상태를 가져와서 이를 가지고 랜더링하려면 일단 위에 보시다시피 로딩시간이 필요로 되어진다.

내비게이션 바의 경우에는 이 로딩시간이 사용자에겐 어색한 사용자 경험이 될 수 있다. 그렇다.... Recoil을 쓰는 방식은 안된다...




👏 최종 해결 방법

위와 같은 내용을 같은 팀원에게 알려줬고,논의를 해본 결과 다음과 같이 페이지 구조를 만들기로 했다.

내비게이션이 있는 페이지, 없는 페이지 두개로 나눈 후에, 내비게이션이 있는 페이지를 view라는 새로운 폴더에 담아두기로 했다. 이 폴더에 layout.jsx를 만들어서 이 안에 내비게이션 바를 만들자는 해결책이다.

또한 내비게이션 바의 상태를 관리할때는 주소에 값을 넘겨주는 방식을 채택하기로 했다.
예를 들면 아래와 같이 말이다
/view/home?tab=Home
이런식으로 tab에 Home값을 넘겨주면 그 값에 따라서 내비게이션 바를 동작하게 하였다.

이렇게 한 결과를 한번 보자.

예상한대로 잘 나오는 것을 볼 수 있다.


이렇게 만든다면 장단점이 있다.

  • 장점
  1. 우선, 이렇게 만든다면 리코일 같은 전역 상태 관리 라이브러리를 사용하지 않아도 된다.
    -> 이렇게 됨에 따라 이전에 발생했던 문제들이 모두 해소된다. 리코일을 써서 관련 데이터를 불러오는 시간? 전혀 필요없다.
  2. 만들기 쉽다. 사실 이 방법을 생각하지 못한건 아니다. 우선 완성도 높은 프로젝트를 만들기 위해 리코일이라는 기술을 써보고 싶어서 쓴것도 있지만, 가장 큰 이유는 단점들에서 나온다.
    -> 하지만 이 단점들을 감수하고 만든다면 너무나도 만들기 쉽고 간단하고 명확하다.
  • 단점
  1. 가장 큰 단점은 페이지 URL이 복잡해진다.
    -> 사실 많이 복잡해지는건 아니지만 위에 예시처럼 Next.js특성상 페이지를 묶는 폴더를 만들면 그 폴더명이 곳 URL이 된다.
    다시 말해, /view/home?tab=Home 이런식으로 view라는 폴더에 묶는다면 view/~~~ 이런식으로 접근해야한다.
    이쁘지 않다는 것이다...
  2. URL에 값을 던져주는 방식을 하게 되면, 다른 페이지에서 이 내비게이션바를 사용하게끔 업데이트를 해줘야 한다면 난감해질 수 있다.
    모든 사용하는 페이지 뒤에 tab=값을 넘겨줘야 한다. 굉장히 귀찮은 일이다.




✍ 마무리

위에 여러가지 문제를 겪으면서 각각의 해결책을 내기 위해 공부하고 또 공부했다.
그러면서 리코일이나 SSR,CSR 같은 개념들에 더욱 더 깊이 있게 공부했던 경험이었다. 좀 아쉬웠던 부분이라면.... 폴더 구조가 깔끔하지 못하게 해결했다는 점이다.

본인이 지금 생각했을때 가장 좋은 방법은 최상위 layout.tsx에 내비게이션 바를 넣고 이 layout.tsx를 원할때만 표시하게 하는 방식? 이 가능하다면 깔끔하다고 생각한다.

하지만 이를 공부했을땐 방법을 찾지 못했고, 위와 같은 방식으로 해결하게 되었다.

이러한 프로젝트에서 발생한 문제를 통해 Trouble Shooting 과정을 거치면서 Next.js라던지, SSR,CSR 이라던지 TS라던지 관련 개념들을 더욱 더 확실히 가져가는 것 같다.

Next.js를 통해 본인과 같은 문제가 발생했다면 이 블로그가 도움이 됐으면 좋겠다.

profile
집돌이 FE개발자의 노트

1개의 댓글

comment-user-thumbnail
2023년 8월 3일

좋은 정보 얻어갑니다, 감사합니다.

답글 달기