본문 바로가기
> 레벨업의 테크노트

[프론트엔드] React 뒤로가기 시 상태값 & 스크롤 위치 유지

by 쫑's 2022. 4. 5.

안녕하세요! 빅픽처인터렉티브(주)에서 프론트엔드 개발자로 근무하고 있는 김종완 개발자입니다!

 

이번에 개인화앱 TF에 배정이 되어 재밌게 새로운 서비스를 만들어가고 있는데요.

 

사내에 많은 프론트엔드 개발자분들이 React를 활용하여 프로젝트를 진행하고 싶다는 Needs가 있다고 판단하여

이번에 들어가는 개인화앱은 React를 활용하여 모바일 웹뿐만 아니라 안드로이드도 하이브리드 앱으로 개발을 진행하기로 했습니다.

 

이전에도 많은 하이브리드앱을 개발해본 경험이 있는데요.

Vue 프레임웍을 활용해서 하이브리드 앱을 개발해본 경험은 다수 존재하는데 React 라이브러리를 활용해서

하이브리드 앱을 개발해본 경험은 별로 없는 거 같습니다.

 

하이브리드 앱뿐만 아니라 커머스 같은 서비스를 개발하면서 제가 제일 많이 고민하는 건

'뒤로 가기 시 상태 저장을 해야 할 때가 많은데 어떻게 하면 쉽고 이질감 없이 저장하고 스크롤 위치를 유지할 수 있을까?'였습니다.

 

 

예를 들어 우리가 어떤 쇼핑앱이나 쇼핑몰에서 상품 목록을 구경하며 쇼핑을 즐기고 있었습니다.

그런데 한참 스크롤을 내리던 중 호기심이 가는 상품을 발견하였고 해당 상품을 눌러 상품에 상세 정보를 보다가 생각보다 별로 마음에 들지 않아 뒤로 가기를 했을 때 상품 목록 페이지의 스크롤이 다시 최상단으로 가있다면...

 

 

유저 입장에서는 굉장히 당황스럽고 짜증나는 경험이 아닐 수 없을 겁니다.

 

이 때문에 유저 이탈률(Bouce Rate)가 높아져 갈 것이며 저희 서비스에 대한 사용자 경험은 굉장히 나쁜 경험으로 남게 되겠죠.

오늘은 이 부분에 대해서 고민했던 과정과 고민 끝에 나온 결과물에 대해 공유하고 피드백을 받아보고자 글을 작성해보려고 합니다.

 

 

스크롤 위치를 기록해놓고 뒤로 가기 시 스크롤 위치 조정

 

처음 시도해보았던 건 구글링 했을 시 가장 많이 나오던 스크롤 위치를 localStorage나 sessionStorage에 기록해놓고 뒤로 가기 했을 시 다시 스크롤 위치를 재조정하는 방법이었습니다.

 

뭐 제가 생각해도 스크롤 위치만 원래 위치에 있도록만 한다면 나쁘지 않을 거 같았습니다..

제가 1차적으로 얻고 싶었던 건 사용자가 목록을 보다가 상세페이지로 간 뒤 다시 뒤로 가기 했을 시 보고 있던 목록 위치가 그대로 표시되는 거였으니까요.

 

그래서 구현을 해보았습니다.

 

useScrollMove.ts

import { useState, useEffect, useCallback, useRef } from 'react';
import { useNavigate } from 'react-router';
import { Link, useLocation } from "react-router-dom";
import _debounce from 'lodash/debounce';

interface PropsType {
    page: string;
    path: string;
    dom?: any;
}

const useScrollMove = ({ page, path, dom } : PropsType) => {
  const location = useLocation();
  const scrollY = useRef(Number(localStorage.getItem(`${page}_scroll_pos`)));

  const scrollRemove = useCallback(() => {
    scrollY.current = 0;
    localStorage.removeItem(`${page}_scroll_pos`);
  }, []);

  const saveScroll = _debounce(function() {
    scrollY.current = window.pageYOffset;
  }, 100)

  useEffect(() => {
      window.addEventListener('scroll', saveScroll);
    return () => {
        window.removeEventListener('scroll', saveScroll);
        localStorage.setItem(`${page}_scroll_pos`, String(scrollY.current));
    };
  }, []);

  return { scrollInfos: scrollY.current, scrollRemove };
};

export default useScrollMove;

소스를 보면 아시겠지만 스크롤 이벤트 리스너를 추가하고 스크롤 될 때마다 ref객체에 특정 key값으로 스크롤 offset을 기록해놓고 unmount가 될때 localStorage에 저장하도록 하며 추가로 localStorage에 저장해놓은 스크롤 값을 remove 할 수 있는 scrollRemove 함수와 스크롤 offset 값을 반환하도록 커스텀 훅이 개발되어있습니다.

 

 

이 커스텀 훅의 사용법은 굉장히 번거롭습니다.

 

const { scrollInfos, scrollRemove } = useScrollMove({
    page: 'notice-list',
    path: '/notice',
});

.....

useEffect(() => {
    getNotice(list.length); // 리스트를 가져오도록 API를 호출하고 리스트를 저장하는 함수
}, []);

useEffect(() => {
    if (scrollInfos) {
      if (window.isBack) {
        window.scrollTo(0, scrollInfos);
      } else {
        scrollRemove();
      }
      const scrollTop = Math.max(
        document.documentElement.scrollTop,
        document.body.scrollTop
      );
      //현재위치와 복구위치가 같다면
      if (scrollTop == scrollInfos) {
        scrollRemove();
        window.isBack = false;
      }
    } else window.isBack = false;

    //의존성 배열에 fetching 해오는 데이터를 넣어준다.
}, [scrollInfos, scrollRemove, list]);

 

보시다시피 매번 스크롤 위치를 재조정해줘야 하는 컴포넌트 안에 useEffect로 이전 기록된 스크롤 값과 현재 스크롤 값이 같을 때까지

list 값을 보면서 무한 스크롤이 적용된 리스트 API를 반복적으로 호출을 해주고 있습니다.

 

이전 스크롤 값과 현재 스크롤 값이 같아지면 이전 페이지에서 불러왔던 리스트 목록과 동일하게 불러왔다 판단하고 스크롤 위치도 원래 있었던 위치로 조정되고 리스트 API도 더 이상 호출하지 않습니다.

 

이 소스의 결과물은 이렇습니다..

 

 

잘 작동은 되나 스크롤 위치가 재조정되는 부분에서 조금 부자연스럽게 작동이 되는 걸 보실 수 있습니다.

 

제 목표가 사용하기 쉽고 이질감 없이 이전 상태를 유지해야 한다라는 걸 감안하다면 만족스럽지 않은 결과물인 거 같습니다.

 

이전 페이지로 이동시 기존 불러왔던 API request를 또 다시 날리면서 그 사이에 저와 같이 투둑(?)하며 스크롤 위치가 재조정 되는 것이라고 추측되었으며 state를 저장하지 않으면 저렇게 또 다시 많은 API request를 날리는것 또한 상당히 비효율적이라고 판단하였습니다.

 

그렇다면 이전페이지로 이동했을 때 스크롤 위치뿐만 아니라 state까지 저장해야 한다고 생각하였고 다른 방법을 찾아보기로 하였습니다.

 

 

 

Axios cache를 이용하면 불필요한 API 요청을 하지 않아도 될 거 같은데?

 

위와 같은 고민을 하다가 Axios cache를 이용하면 cache 처리된 이전 request를 이용하면 불필요한 네트워크 요청을 줄이고 이질감 없이 위 소스를 활용하면 스크롤 위치를 자연스럽게 재조정할 수 있지 않을까 하는 생각이 들었습니다.

 

하지만 이 고민은 오래가지 않았습니다.

 

무한 스크롤 시 한 페이지에서 불러왔던 리스트의 offset을 저장해놓고 그다음부터는 해당 offset에서 n개의 리스트를 더 가져올 수 있도록 API 요청을 합니다. 근데 캐싱 처리된 request를 이용한다면 이 페이지에서 불러왔던 모든 request를 캐시로 처리할 수 있을까? 하는 우려로 인해 그냥 이전 상태를 그대로 기록해놓거나 캐시 처리해서 initiallizing 되도록 하는 KeepAlive 같은 역할을 하는 라이브러리가 있는지 찾아보기로 결정하였습니다.

 

이 시도는 해보지 않았기 때문에 혹시나 제 추측이 틀렸다고 생각하시거나 시도를 해보신 경험이 있으시다면 피드백 주시면 감사하겠습니다!

 

 

 

Vue와 같은 KeepAlive기능이 React에도...?

 

처음 고민한 건 'React에도 Vue처럼 KeepAlive 기능이 지원이 되는가?'였습니다.

Vue는 KeepAlive라는 기능을 제공함으로써 해당 페이지에서 벗어나더라도 그 페이지(컴포넌트)의 구성 요소 인스턴스를 조건부로 캐시 할 수 있는 기본 제공 구성 요소입니다.

 

 

페이지 이동시 destoryed 되는 것이 아니라 deactivated가 되고 다시 해당 페이지로 진입하였을 때는 mounted가 되는 것이 아니라 activated가 되도록 하여 페이지(컴포넌트)의 구성 요소 인스턴스를 캐시 처리하여 다시 보여줍니다.

예전 프로젝트 중 이 KeepAlive를 이용하여 뒤로 가기 처리를 하였고 생각보다 굉장히 잘 작동하는 걸 확인했던 경험이 있어서 우선 React에도 KeepAlive를 지원하는지 확인을 해보았습니다!

하지만 아쉽게도 React는 Vue와 달리 프레임웍이 아닌 라이브러리이기 때문에 React 자체에서 KeepAlive 같은 기능을 제공하지 않고 있었습니다.

 

 

 

그렇다면 다른 유능한 개발자들이 또 만들어놨겠지!!!

 

그렇습니다! 이 세상에는 유능한 프론트엔드 개발자들이 아주 많으며 우리에게는 구글이 있습니다!

바로 구글링을 해보았고 역시나! Vue 프레임웍에서 제공하는 KeepAlive 같은 기능을 제공하는 라이브러리들을 발견하게 되었습니다.

 

 

1. react-keep-alive

 

react-keep-alive는 구성 요소 상태를 유지하고 반복되는 rerender을 방지하도록 개발이 되어있는 라이브러리입니다.

Vue 프레임웍의 KeepAlive 기능을 최대한 비슷하게 구현하려 한 거 같았고 React.createPortal API를 이용하여 Provider에 저장해놓은 캐시 정보를 KeepAlive 컴포넌트에다가 노출 및 표시를 하도록 위치를 옮기게 구현이 되어있습니다.

 

이 라이브러리는 LifeCycle 2개가 추가됩니다.

추가되는 LifeCycle은 componentDidActivate와 componentWillUnactivate입니다.

 

componentDidActivate초기 마운트 후 또는 활성화되지 않은 상태에서 활성 상태로 한 번 실행됩니다. componentDidActivate단계에서 이후 componentDidUpdate를 볼 수 있지만 이것이 항상 트리거 됨을 Updating 의미하지는 않습니다. componentDidActivate 동시에 componentWillUnactivate 및 의 수명 주기 중 하나만 componentWillUnmount트리거 됩니다. componentWillUnactivate캐싱이 필요할 때 실행됩니다. componentWillUnmount캐싱 없이 실행됩니다.

[출처: https://www.npmjs.com/package/react-keep-alive]

 

 

해당 라이브러리를 쓸까.. 고민을 하다가 쓰지 않게 된 이유는 현재는 업데이트되지 않은지가 거의 3년이 다돼가고 있다는 문제점 때문입니다.

현재 리액트 라우트가 버전 6까지 나온 상태인데 현 소스를 보면 예전 리액트 라우터 버전을 쓰기도 하고 있고 지속적으로 관리가 되고 있지 않은 거 같아서 이슈가 있을 때 또 다른 사이드 이펙트들이 발생할 거 같다는 개인적인 우려로 인해 해당 라이브러리는 쓰지 않기로 결정을 하게 되었습니다.

 

 

2. react-router-cache-route

 

해당 라이브러리는 리액트 라우트의 Route컴포넌트를 CacheRoute라는 컴포넌트로 대체하여 props와 2가지의 추가된 LifeCycle을 이용하여 상태를 유지하도록 구현되어 있습니다.

구현 스펙은 React v15+, React-Router v4+ 로 구성되어 있습니다.

 

이 라이브러리를 활용해서 구현한 샘플 영상(?)을 한번 보면 제가 생각하던 자연스럽고 간단하게 이전 상태를 기억하고 이질감 없이 작동하는 걸 확인할 수 있습니다.

 

[출처: https://github.com/CJY0208/react-router-cache-route]

 

 

위 영상의 디버깅 창을 보면 이전 페이지가 unmount가 되는 것이 아니라 display:none; 으로 안 보이게 처리하고 다시 페이지가 돌아왔을 시 display:none; 처리를 없애서 이전 페이지를 이전 상태 그대로 보여주도록 처리한 것 같습니다.

 

사실 해당 라이브러리를 보기 전에 비슷한 처리방식을 제안해주신 분이 전적 검색 TF에 계신 이한종 프론트엔드 개발자이신데 dom이 너무 거대해지지 않을까? 하는 생각이 들기도 하였지만 나쁘지 않은 처리 방식이겠다 라는 생각을 했었던 기억이 나네요..!

 

그럼에도 불구하고 이 라이브러리를 선택하지 않았습니다.

이유는 react-keep-alive와 마찬가지로 리액트 라우트 버전 6을 지원하는지 불분명하고 만들어진 컴포넌트를 보면 리액트 라우터 버전 4나 5에서 작동할 수 있게 구현이 되어있는 거 같았습니다.

 

'이 라이브러리를 쓰기 위해 이전 리액트 라우트 버전을 쓰는 게 맞는 걸까?'라는 고민 끝에 알 수 없는 사이드 이펙트를 감수하고 제가 수정하지 못하고 커스텀도 힘든 이 라이브러리를 쓰기가 조금 두려운 느낌이 들어서 과감히 PASS 하는 걸로 결정하게 되었습니다.

 

 

 

마지막 방법은 역시... 직접 개발이겠지...?

 

정리해보면 이렇습니다. 이전 state, ref를 기록해놓고 스크롤 위치 또한 기록을 해놓으며 뒤로 가기를 구별해놓을 수 있는 아주 사용하기 쉽고 이질감 없이 뒤로가기 상태 유지를 할 수 있는 기능..!!

 

그리고 위 라이브러리들을 보았을 때 뒤로 가기를 했을 때만 작동할 LifeCycle 또는 useEffect 같은 훅이 필요해 보였습니다.

 

일단 뒤로 가기를 했을 시 보이는 페이지에 속해 있는 컴포넌트 중 상태 값을 유지해야하는 컴포넌트들이 이용할 커스텀 훅을 만들어서 그 커스텀 훅을 활용해 커스텀훅 자체에서 바로 상태값을 저장하고 스크롤 값을 저장&재조정해주는 기능을 넣기로 했습니다.

 

import React, { createContext, useLayoutEffect, useContext, useRef, useState } from 'react';
import { BrowserHistory } from 'history';
import { UNSAFE_NavigationContext, useLocation } from 'react-router-dom';

...

export const BackHistoryContext = createContext(initialValue);

const BackHistoryProvider = ({ children }: PropsType) => {
  const location = useLocation();
  const [firstLoad, setFirstLoad] = useState(true);
  const backhistoryStore = useRef(initialValue.current);

  const navigation = useContext(UNSAFE_NavigationContext)
    .navigator as BrowserHistory;

  useLayoutEffect(() => {
    if (!firstLoad && navigation) {
      if (navigation.action === 'POP') {
        backhistoryStore.current.isBack = true;
      } else {
        backhistoryStore.current.isBack = false;
      }
    }

    if (firstLoad) setFirstLoad(false);
  }, [location]);

  return (
    <BackHistoryContext.Provider value={backhistoryStore}>
      {children}
    </BackHistoryContext.Provider>
  );
};

export default BackHistoryProvider;

 

먼저 뒤로 가기&앞으로 가기 이벤트 즉, navigation action이 push인지 pop인지를 구분하여 저장하는 기능을 만들어야 했습니다.

여기서 유의하여 봐야 하는 것은 useLayoutEffect 훅인데요. 이 훅은 화면이 업데이트되기 전에 실행되는 훅입니다.

 

useEffect

  • 컴포넌트 렌더링 - 화면 업데이트 - useEffect실행

useLayoutEffect

  • 컴포넌트 렌더링 - useLayoutEffect 실행 - 화면 업데이트
useEffect와 useLayoutEffect의 차이를 좀 더 자세히 알고 싶으시다면 https://velog.io/@suyeonme/react-useLayout%EA%B3%BC-useEffect%EC%9D%98-%EC%B0%A8%EC%9D%B4 이 url에 접속하셔서 확인해주세요~

 

화면이 업데이트되기 전 그리고 useEffect가 실행되기 전에 먼저 페이지 이동 action을 파악하고 뒤로 가기&앞으로 가기 인지 아닌지를 조건부로 boolean 타입 isBack 변수에 저장을 해줍니다.

다음 각 컴포넌트의 상태 값을 저장하기 위해 만든 초기화된 객체인 backHistory 객체와 isBack 변수를 객체에 담아서 ref 값으로 초기화 & 저장을 해줍니다.

 

여기서 firstLoad라는 state값이 보이는데 이 state를 만들어준 이유는 페이지 새로고침 및 처음 페이지에 진입했을 때 페이지 이동 action이 push가 아닌 pop으로 인지를 하기 때문에 boolean타입 state를 만들어 처음 사이트가 랜더링 되었을 때는 예외처리를 하였습니다.

이를 Context API를 활용하여 자식 컴포넌트들이 얼마든지 ref값에 접근하고 값을 변경하도록 구현하였습니다.

 

여기서 state가 아닌 ref로 저장을 한 이유는 Provider의 state값이 변하면 자식 컴포넌트들도 모두 rerender가 되기 때문에 불필요한 rerender를 방지하기 위해 ref로 활용하였습니다.

 

이제 useBackControl이라는 커스텀 훅을 만들어 현재 페이지의 상태를 저장하도록 구현해야 합니다.

 

const useBackControl = <T = { [key: string]: any }, G = { [key: string]: MutableRefObject<any> }>(
  prefixKey?: string
) => {
  const [load, setLoad] = useState(false);
  const backContext = useContext(BackHistoryContext).current;
  const key = location.href + (prefixKey ?? '');

  window.__BACK_HISTORY__ = backContext.backHistory; // 디버깅하기 쉽게 window객체에도 담아서 보여주도록 설정

  if(!backContext.backHistory[key]){
    backContext.backHistory[key] = {
      scrollPos: 0,
      state : {} as T,
      ref: {} as G
    }
  }


...

 

useBackControl이라는 커스텀 훅을 만들고 뒤로 가기 시 각 컴포넌트들의 상태를 구별하여 기록해 저장하기 위해 각 컴포넌트의 key값을 location.href와 추가로 덧붙일 prefixKey 값을 전달 인자로 받아 이전 만들어준 Provider의 context를 backContext [key] 객체를 초기화해줍니다.

 

여기서 왜 location.href를 포함하여 key값을 설정하는지 궁금하실 거라 생각이 됩니다.

로컬로 개발을 했을 시 여러 프로젝트를 진행하다 보면 포트번호가 유동적으로 바뀔 수 있고 도메인이 바뀔 수도 있다고 판단하여서 location.href를 덧붙여서 key값을 설정하는 건데 사실.. pathname과 query string만으로 키값을 설정해도 문제가 없어 보이긴 하네요..

 

...

const useMount = (func: Function, deps: any[]) =>
    useEffect(() => {
      const isBack = backContext.isBack && Boolean(backContext.backHistory[key]);
      if (!isBack) {
        func();
      }
}, [...deps]);

const useActive = (func: Function, deps: any[]) =>
    useEffect(() => {
      const isBack = backContext.isBack && Boolean(backContext.backHistory[key]);
      if (isBack) {
        setTimeout(() => {
          func();
        }, 1);
      }
}, [...deps]);

....

 

그 다음은 뒤로 가기&앞으로 가기 이벤트가 발생했을 때만 작동하는 useEffect와 그 반대의 경우만 작동하는 useEffect 훅을 생성하는 작업을 useBackControl 커스텀 훅 내에서 진행합니다.

 

backContext.isBack은 위에서 페이지 이동의 action이 'push'인지 'pop'인지를 확인하여 boolean 타입으로 저장한 값이며 action이 'pop'일 경우 뒤로 가기나 앞으로 가기 이벤트가 발생한 것이기 때문에 true 값을 가지게 될 겁니다. 또한 기록해놓은 이전 상태가 있는지를 확인하여 이 컴포넌트를 자식으로 두고 있는 페이지가 정말 뒤로 가기/앞으로 가기로 이동해 왔는지 아닌지를 구분하여 줍니다.

 

즉 이 컴포넌트가 뒤로 가기&앞으로 가기 이벤트가 발생이 된 상태로 랜더링이 되었고 기억해놓은 state, ref, 스크롤 값이 있을 경우만 useActive에 첫 번째 인자로 전달한 함수가 실행되고 그게 아닐경우 useMount에 첫번째 인자로 전달한 함수가 실행되게끔 정의를 한 것입니다.

 

이로써 저희는 useEffect훅처럼 작동하지만 뒤로 가기&앞으로 가기를 했는지 안 했는지에 따라 조건부로 작동하는 훅을 생성하였습니다.

 

다음으로 저는 컴포넌트가 각각 어떤 state, ref 값을 갖고 있는지 값이 mutation 되었을 경우 그것을 감지하고 변경된 값을 다시 backContext.backHistory객체 안에 컴포넌트의 키값으로 저장되게 하는 기능을 구현해야 했습니다.

 

여기서 정말 많은 고민을 했었는데요. Vue에 친숙한 저는 Vue 객체에 접근하여 컴포넌트, 그 컴포넌트의 data값을 추적하는 방식이 익숙하다 보니 React는 어떻게 state값을 감지하고 알아내고 변경한 값을 기록할 수 있을까에 많은 고민을 하였습니다.

그러다가 React는 state값을 initiallizing할 때 useState라는 훅을 쓴다는 것과 ref를 initiallizing 할때 useRef라는 훅을 쓴다는 것에 초점을 두게 되었습니다.

 

잘 생각해보면 useActive와 useMount 훅을 useEffect훅을 활용해 정의할 때처럼 똑같이 useState와 useRef훅을 활용하면 컴포넌트가 어떤 state값과 ref를 정의하는지 그리고 값이 mutation 될 때도 동일하게 같이 적용되도록 할 수 있겠다 라는 생각을 하게 되었습니다.

 

...

const useRemState = <S>(state: S, keyName: string) => {
    const memoryValue = backContext.backHistory[key]?.state[keyName as string] ?? state;

    const resultState = useState<S>(memoryValue as S);

    if(load) (backContext.backHistory[key] as BackHistoryType).state[keyName] = resultState[0];

    return resultState;
};

const useRemRef = <S>(ref: S, keyName: string) => {
    const memoryValue = backContext.backHistory[key]?.ref[keyName as string]?.current ?? ref;

    const resultRef = useRef(memoryValue as S);

    if(load) (backContext.backHistory[key] as BackHistoryType).ref[keyName] = resultRef;

    return resultRef;
};

...

useLayoutEffect(() => {
    setLoad(true);
}, []);

...

 

위 소스를 보시다시피 useState와 useRef를 활용한 새로운 훅을 생성하도록 하고 첫 번째 인자로는 처음 initiallizing할때 초기화 해주고 싶은 값을 넣어주고 state or ref값의 변수명을 두번째 인자인 keyName에 전달하여 컴포넌트가 갖는 state or ref가 무엇인지 어떠한 값을 갖고 mutation 되는지 추적하고 기록할 수 있도록 하는 useRemState와 useRemRef 함수를 생성하였습니다.

 

backContext.backHistory객체 내에 기록되어있는 값이 있으면 기록해놓은 state or ref값으로, 아니면 원래 초기화를 해주고 싶었던 값인 첫번째 전달 인자 값으로 initiallizing을 해주도록 하였습니다.

 

여기서 유의 깊게 보셔야 하는 부분은 if(load) ... 부분입니다.

 

아까 useLayoutEffect 훅을 간략히 설명드렸는데요. useEffect보다 useLayoutEffect 훅이 먼저 실행이 되지만 useState와 useRef는 이보다 더 먼저 실행이 됩니다. 그렇기 때문에 useLayoutEffect훅이 실행될 때 load라는 boolean타입의 state값이 false에서 true로 변경되며 load가 true가 된 이후부터 backContext.backHistory[key] 객체에 각 컴포넌트들의 상태 값을 기록할 수 있도록 하기 위한 부분입니다.

 

마지막으로 스크롤 이동시 이벤트 리스너를 통하여 스크롤 위치 값을 저장하고 뒤로가기&앞으로가기 이벤트 발생시 저장한 스크롤값을 활용해 스크롤 위치값을 재조정하는 기능을 개발해보도록 하겠습니다.

 

...

    useEffect(() => {
        const isBack = backContext.isBack && Boolean(backContext.backHistory[key]?.scrollPos);

        if (isBack) {
          window.scrollTo(0, backContext.backHistory[key]?.scrollPos ?? 0);
        }

        let scrollPos = 0;

        const saveScroll = _debounce(function () {
          scrollPos = window.scrollY || window.pageYOffset;
        }, 100);

        window.addEventListener('scroll', saveScroll);

        return () => {
          window.removeEventListener('scroll', saveScroll);
          (backContext.backHistory[key] as BackHistoryType).scrollPos = scrollPos;
        };
    }, []);

    ...

  return {
    useMount,
    useActive,
    useRemState,
    useRemRef,
  };
};

export default useBackControl;

 

backContext.isBack이 true고 미리 기록해놓은 이전 스크롤 위치 값인 backContext.backHistory[key].scrollPos 값이 존재하고 0이 아니면 스크롤 값을 재조정해주기 위해 window.scrollTo 함수를 호출해주어 스크롤 위치를 재조정해줍니다.

 

useBackControl 커스텀 훅을 각 컴포넌트에서 호출하는 것만으로 자동으로 작동을 하기 때문에 이제 저희는 전혀 번거롭지 않고 쉽게 스크롤 위치를 재조정할 수 있습니다.

 

이제 밑에 소스와 같이 상태를 저장하고자 하는 컴포넌트에 useBackControl 커스텀 훅을 호출하고 반환되는 훅들을 활용하면 손쉽고 간편하게 이전 상태를 저장할 수 있습니다.

 

const Home = () => {
  ...

  const {useMount, useActive, useRemState} = useBackControl('Home');

  ...

  const [tabIndex, setTabIndex] = useRemState(0, 'tabIndex');
  const [swiper, setSwiper] = useState<any>(null);
  const [allList, setAllList] = useRemState<ConvertGameDataType[] | null>(null, 'allList'); 
  const [myList, setMyList] = useRemState<ConvertGameDataType[] | null>(null, 'myList'); 

  const getAllMatchRoom = async () => {
    const allList = await services.matchRoom.getRooms();
    setAllList(allList.rooms as ConvertGameDataType[]);
  }

  useEffect(() => {
    swiper && swiper.slideTo(tabIndex);
  }, [tabIndex]);

  useMount(() => {
    getAllMatchRoom();
    setMyList([]);
  }, []);

 

주의해야 할 점은 useBackControl 커스텀 훅에서 반환하는 useRemState, useRemRef 훅은 useState, useRef와는 다르게 2번째 인자에 꼭 key값을 넣어줘야 한다는 점입니다. 이것 말고는 딱히 신경 쓰지 않고 기존 훅을 쓰시던 것처럼 사용하시면 됩니다.

 

물론 useBackControl 커스텀 훅에서 제공하는 훅들을 사용하지 않아도 React에서 제공하는 기존 useState와 useEffect 훅 등을 사용할 수 있습니다. 하지만 기존 훅을 사용하시면 상태 값은 저장 및 기록하지 못한다는 것! 기억해주세요~!

 

 

그럼 잘 작동되는지 확인해볼까요?

이질감 없이 그리고 불필요한 네트워크 요청을 다시 하지 않으며 상태 값을 잘 유지하는 걸 보실 수 있습니다!

 

# 마치며..

이 기능을 구현하면서 정말 많은 고민을 했던 거 같습니다. 처음에는 window객체에 담아서 해보기도 하고... 샤워하면서도 고민하고 멍 때리면서도 고민하고.. 그렇게 길게 고민하면서 이 기능을 굳이 만들었던 이유는 저희 FE 팀원들이 만약 이 기능을 사용하게 되면 정말 쉽고 간편하게 사용을 했으면 좋겠다 라는 생각 때문에 좀 더 간편하게 좀 더 우아하게(?) 좀 더 이해하기 쉽게 짜려고 노력했던 거 같습니다.

물론 아직 많이 부족한 컨텐스 트이지만 글을 보시다 피드백 줄 점이 있다면 언제든지 주시면 감사하겠습니다!

 

같이 고민해주시고 의견을 주셨던 허남진 님, 김병규 님, 이상욱 님, 이한종 님 너무나 감사했습니다~! 

 

더욱더 고민하고 더 발전하는 프론트엔드 개발자가 되도록 노력하겠습니다~~~ 

댓글