우연히 서핏에서 리액트 관련 블로그 글을 보다가 NHN의 TOAST 기술 블로그에서 Jotai 라이브러리에 있는 atomWithHash 로 모달 상태를 URL과 동기적으로 관리하는 방법에 대해 소개하는 글을 보았습니다.
회사에서 상태관리 라이브러리로 Redux Store를 사용하고 있어서 Jotai 를 같이 사용할 순 없을 것 같았지만, URL로 상태관리를 한다는 컨샙 자체가 저에겐 상당히 신선했습니다.
거미줄 처럼 엮여있는 지저분한 수 많은 상태 관리용 boolean 값을 모두 없애버릴 수 있는 절호의 기회라는 생각이 가장 크게 들었고, 기존의 라우터와는 독립적으로 동작하기 때문에 라우터나 기존의 코드 사이에 코드를 끼워넣지 않아도 된다는 점이 큰 매리트라고 생각이 되어서 당장 회사에 가서 도입해볼 마음에 가슴이 두근두근했습니다.
앞에서 제가 말하는 서브페이지란, 사이드바의 사이드바 정도 되는 floating window 를 의미합니다. 저희 사이트에서는 로그인, 회원가입, 마이페이지 등이 여기에 합니다. 현재 주소와 관계없이 여러 주소의 여러 상황에서 화면에 나타날 수 있고, 한번에 한 종류의 창만 보입니다. 즉, 로그인 / 회원가입 / 패스워드 변경 / 마이페이지 등 여러 종류가 있는데 한번에 한 종류의 창만 보이게 됩니다.
- 여러 종류의 창이 한번에 화면에 보여질 수 있는 경우는 아직 생각해보진 않았습니다. 아마도 hash 값에 배열 형태로 페이지를 넣어서 따로 관리해주는 로직을 추가해줘야 되지 않을까 싶네요. 왜냐면 hash는 주소당 1개만 사용할 수 있습니다.
우선 라우터 최상위에 SubPageRoute를 추가해서 location.hash 값에 따라서 서브페이지를 변경하도록 만들었습니다.
그리고
<Routes>
<Route element={<SubPageRoute />}>
... 기존 라우터들
</Route>
</Routes>
const SubPageRoute = () => {
const location = useLocation()
return (
<>
<Portal id="#subpage">
{location.hash === `#subpage=${SUBPAGE.로그인}` ? (
<로그인/>
) : location.hash === `#subpage=${SUBPAGE.패스워드변경}` ? (
<패스워드변경/>
) : location.hash === `#subpage=${SUBPAGE.회원가입}` ? (
<회원가입/>
) : location.hash === `#subpage=${SUBPAGE.마이페이지}` ? (
<마이페이지/>
) :
...
(
<></>
)}
</Portal>
<Outlet />
</>
)
}
참고로 Portal 컴포넌트는 하위 컴포넌트를 기본 root DOM 의 아래에 렌더링 하지 않고 별도의 dom 하위에 렌더링 해주는 커스텀 컴포넌트 입니다. z-index 문제에서 벗어날 수 있고 DOM 구조상 깔끔해보여서 개인적으로 아주 좋아합니다.
- 리액트 공식 사이트 : https://ko.reactjs.org/docs/portals.html
Portal 컴포넌트 코드는 아래와 같습니다.
그리고 public/index.html 파일에
import React from 'react'
import ReactDOM from 'react-dom'
interface PortalProps {
id: string
children: React.ReactNode
}
const Portal = ({ id, children }: PortalProps) => {
const target = document.querySelector(id)
return target ? ReactDOM.createPortal(children, target) : <></>
}
export default Portal
그리고 public/index.html 파일의 body 부분에 아래와 같이 추가해주면 됩니다.
<body>
<div id="root"></div>
<div id="subpage"></div>
<div id="modal"></div>
<div id="popup"></div>
</body>
서브페이지 열고 닫기는 아래와 같이 해줬습니다.
버튼을 눌렀을때 페이지 바꾸려면 useNavigate 훅의 navigate()를 썼고,
조건에 따라 컴포넌트 내용 보여주지 않고 페이지 리다이렉트 시키고 싶을땐 Navigate 를 사용했습니다.
서브페이지 열기
const navigate = useNavigate()
navigate(`${window.location.pathname}#subpage=${SUBPAGE.로그인}`)
혹은
return (
<Navigate to={`${window.location.pathname}#subpage=${SUBPAGE.로그인}`}/>
)
서브페이지 닫기
navigate(window.location.pathname)
혹은
return (
<Navigate to={window.location.pathname} />
)
위의 방법으로 서브페이지를 닫고 나서 브라우저 뒤로가기 버튼을 누르면 평소와 다른 놀라운 일이 일어납니다.
기존의 SPA 페이지에서는 브라우저 뒤로가기 버튼을 누르면 닫혔던 모달이나 서브페이지가 복구되는 것이 아니라 아예 이전 페이지로 돌아가버렸습니다. 하지만, 이제는 뒤로가기 버튼을 누르면 닫혔던 모달이나 서브페이지가 복구됩니다 ! 와 !
뒤로 가기 버튼을 눌렀을 때 이전 상태가 원상복구 되는 것이 UX 적으로도 자연스럽지 않나요? (제 개인 생각임...)
이것이 이제 Default 기능 마냥 탑재되었습니다. 매우 만족스럽네요.
무엇이 어떻게 개선되었는지 다시한번 정리해볼까요?
- Store 내부의 지저분한 상태관리용 boolean 들이 모두 없어졌습니다.
- Before : isLoginComponentOpened, isRestPasswordPage, myPageType 등... boolean에 의한 서브페이지 관리
- After : /주소#subpage="서브페이지명" 처럼 URL Hash로 서브페이지의 상태 관리
- Before : isLoginComponentOpened, isRestPasswordPage, myPageType 등... boolean에 의한 서브페이지 관리
- 기존 URL와 완전 독립적으로 관리
- Before : / 페이지에서 로그인 창이 띄워진 상태의 페이지 직접 접근하려면 /login 라우터를 추가해야하고 마이페이지면 /mypage, 회원가입이면 /join 이렇게 줄줄이 추가하면 되는데, /가 아니라 /abc면 /abc/login ?? 어떻게 해야하지 😅 location.pathname 이 바뀌면 파싱해서 라우팅 처리 해주는 로직을 추가? /abc/def/login 이런 경우도 있을텐데? 😅
- After : /#subpage="login", /abc#subpage="login" 와 같이 앞에 어떤 주소가 오더라도 특정 상태의 화면으로 직접 접근이 가능!
- Before : / 페이지에서 로그인 창이 띄워진 상태의 페이지 직접 접근하려면 /login 라우터를 추가해야하고 마이페이지면 /mypage, 회원가입이면 /join 이렇게 줄줄이 추가하면 되는데, /가 아니라 /abc면 /abc/login ?? 어떻게 해야하지 😅 location.pathname 이 바뀌면 파싱해서 라우팅 처리 해주는 로직을 추가? /abc/def/login 이런 경우도 있을텐데? 😅
- 브라우저 페이지 뒤로가기 클릭시 동작 방식 변경
- Before : 서브페이지 닫기 -> 뒤로가기 클릭 -> 이전 페이지로 되돌아감
- After : 서브페이지 닫기 -> 뒤로가기 클릭 -> 서브페이지 닫히기 전 상태인 열림 상태로 복구
- Before : 서브페이지 닫기 -> 뒤로가기 클릭 -> 이전 페이지로 되돌아감
- 다른 프로젝트로 코드 옮길 때
- Before : Store에 들어있는 값 이동, 라우터도 알맞게 바꿔줘야함, 서브페이지 on/off 하는 dispatch 함수 들도 옮기는 프로젝트에 알맞게 다 바꿔줘야함. 귀찮음.
- After : 그대로 복사 붙여넣기 해도 동작할 정도로 간편하게 코드 옮길 수 있음.
- Before : Store에 들어있는 값 이동, 라우터도 알맞게 바꿔줘야함, 서브페이지 on/off 하는 dispatch 함수 들도 옮기는 프로젝트에 알맞게 다 바꿔줘야함. 귀찮음.
위에서도 언급해지만, URL Hash가 주소당 1개만 쓸 수 있기 때문에 이 체계는 한번에 한 종류의 서브페이지가 나타나는 경우에만 사용가능합니다. 물론 서브페이지가 아니라 모달이나 팝업에 사용할 수도 있고요. 상황에 맞춰서 사용하면 될 것 같습니다.
그리고 여러 종류의 서브페이지들이 동시에 화면에 떠있는 상황은 아마 없을 것 같아서 일단은 추가 개선은 할 생각 없습니다.
그 외에도 상황에 따라 달라지는 사이드바, 메인화면 등을 처리하기 위한 모듈 조립식 라우트 체계 도입에 대해서도 포스팅 작성 예정입니다.
'프론트엔드 개발 > React.js' 카테고리의 다른 글
리액트 장치 유형 감지하기 (0) | 2023.01.15 |
---|---|
React Query onError 가 동작하지 않는 이슈 (0) | 2022.12.21 |
리액트 이론 정리 (JSX, Component, Virtual DOM) (0) | 2022.01.10 |
리액트 props.children 이란 무엇인가? (0) | 2021.12.17 |
React - 프로젝트에서 이미지 경로 찾기 (0) | 2021.11.30 |