디자인시스템을 통해 어드민 사이트의 전체적인 디자인을 바꾸는 일이 있었다.
디자이너의 UI/UX 를 고려한 디자인과 더불어 사용성을 높이는 작업이었는데, 디자인의 통일감은 맞춰졌으나 사용성에 어려움을 겪는 동작들이 하나 둘 생겼다. 그것들을 고치기 위한 여러 작업 중, 손이 많이 갔던 작업이 바로 이 글에 담기는 React.context를 활용한 Anchoring(앵커링)이다.
결론부터 말하자면, 제목에서 알 수 있듯 context를 활용하여 앵커링을 구현했다.
anchoring이라는 거창한 말로 작성했지만, 쉽게 말하자면 그 위치에 고정하는 것이다. 전체를 보여주는 리스트에서 상세 페이지로 갔다 다시 전체 페이지로 왔을 때 내가 이전에 머물렀던 위치로 스크롤이 가 있는 형태를 말한 것이다.
크게 2가지 이유가 있다.
1. 렌더링
2. 무한스크롤
이 간단한..(?) 일을 어떻게 보여주면 될까?
이전에 context 없이 anchoring을 구현했었다.
컴포넌트 외부에 정의한 값은 다시 렌더링 되지 않는 것을 이용해서, 바깥에 미리 변수를 정해두고, Session Storage와 함께 사용하여 구현했었다.
똑같이 이 방법을 쓰면 됐는데, 왜 다른 방법을 찾았을까?
3가지의 이유가 있다.
그래서 방법을 찾다가, Context를 사용하여 구현하는 방법으로 방향을 잡았다.
나는 Context를 사용해서 리스트의 정보를 갖고 있다가 조건에 따라 뿌려주고, 지워주는 형태로 구현했다.
Context는 간단히 말하자면, 내 맥락안에 속하면 내 정보 가질 수 있고 수정도 할 수 있어. 라고 생각한다. 그래서 내 맥락 속 깊게 들어가있는 컴토넌트에서 props drilling 과 같은 번거로움 없이 한 번에 정보를 줄 수도 있다.
글이란 것도 맥락이 참 중요한데.. 아무튼
그래서, 어떻게 구현했는가?
코드를 간단하게 적어보려한다.
//GlobalContext.jsx
import { createContext, useState } from "react";
export const GlobalComponentContext = createContext();
export const GlobalComponentProvider = ({ children }) => {
const gState = {};
// global
[gState.ctxScrollPosition, gState.setCtxScrollPosition] = useState({});
[gState.ctxTotalCount, gState.setCtxTotalCount] = useState({});
// 유저 관리
[gState.ctxUserSearchOption, gState.setCtxUserSearchOption] = useState({});
[gState.ctxUserList, gState.setCtxUserList] = useState([]);
return (
<GlobalComponentContext.Provider value={gState}>
{children}
</GlobalComponentContext.Provider>
);
};
위는 컨텍스트를 생성하고 createContext
, Provider
를
Provider에 보면 useState를 이용하여 값을 저장하려는 것을 알 수 있다.
//GlobalRouter.jsx
<GlobalComponentProvider>
<Routes>
<Route>{loginRouteList}</Route>
<Route element={<PrivateRouter />}>
<Route element={<GlobalLayout />}>{privateRouteList}</Route>
</Route>
</Routes>
</GlobalComponentProvider>
GlobalComponentProvider
를 통해 컨텍스트의 값을 모든 내부 컴포넌트에 지정하는 작업을 해준 것이다. 이미 GlobalComponentProvider
return 값에 value인 gstate
가 ctxUserSearchOption, ctxUserList 등을 모두 담고 있는 커다란 객체가 된다.//ManageUser.jsx
// context
const {
ctxUserList,
setCtxUserList,
ctxScrollPosition,
setCtxScrollPosition,
ctxUserSearchOption,
setCtxUserSearchOption,
ctxTotalCount,
setCtxTotalCount,
} = useContext(GlobalComponentContext);
GlobalComponentContext
는 앞서 만들었던 컨텍스트이다. 그 안에 미리 만들었었던 여러 값을 사용할 수 있다. const getLists = async (obj = filteringOption) => {
setIsLoading(true);
const data = await getSubscribersList(deleteUnusedKey(obj));
setTotalUSer(data.total_count);
setUserLists(data.member);
setCtxUserSearchOption(obj);
setCtxTotalCount({ ...ctxTotalCount, user: data.total_count });
setCtxUserList(data.member);
setStopFetchNew(false);
setIsLoading(false);
};
setCtxUserSearchOption(obj);
setCtxTotalCount({ ...ctxTotalCount, user: data.total_count });
setCtxUserList(data.member);
여기다. 기존 컨텍스트를 사용하지 않았을때는
setTotalUSer(data.total_count);
setUserLists(data.member);
두 개만 해줬다면, 이제는 위의 세 set을 사용하여 데이터를 저장해둔다.
useEffect(() => {
if (isEmptyObj(ctxUserSearchOption) || isEmpty(ctxUserList)) getLists();
}, [filteringOption]);
useEffect(() => {
reloadContext();
}, []);
const reloadContext = () => {
const {
gender,
participation_level,
sort,
min_age,
max_age,
min_uid,
max_uid,
user_name,
subscriber_uid,
nick_name,
} = ctxUserSearchOption;
setFilteringOption({
gender: gender ?? -1,
participation_level: participation_level ?? 0,
sort: sort ?? "UID_DESC",
min_age: min_age ?? null,
max_age: max_age ?? null,
min_uid: min_uid === max_uid ? null : min_uid,
max_uid: min_uid === max_uid ? null : max_uid,
});
if (subscriber_uid) {
setSearchUser(subscriber_uid ?? "");
setIdStatus(SEARCH_STATUS.USER_UID);
}
if (user_name) {
setSearchUser(user_name ?? "");
setIdStatus(SEARCH_STATUS.USER_NAME);
}
if (nick_name) {
setSearchUser(nick_name ?? "");
setIdStatus(SEARCH_STATUS.USER_NICKNAME);
}
setUserLists(ctxUserList ?? []);
setTotalUSer(ctxTotalCount.user ?? 0);
};
const onClickUserDetail = (event, user) => {
event.preventDefault();
navigate(`/user/detail?subscriber_uid=${user.subscriber_uid}`);
setCtxScrollPosition({
...ctxScrollPosition,
user: document.getElementById("user-list").scrollTop,
});
};
const userListRef = useCallback(
(node) => {
if (node !== null) {
node.scrollTo(0, ctxScrollPosition.user);
}
},
[ctxScrollPosition.user],
);
return
<div
className="cp-scrollbar max-h-[70vh]"
ref={userListRef}
id="user-list">
{userLists?.map((user) => (
<div
...
설명이 조금 부족한 것은 추후에 시간이 되면 더 보완하여 작성해봐야겠다.
위에서 설명한,
리스트 보여줘 - 스크롤 하다가 원하는 부분 클릭해 - 다시 뒤로 왔을 때 그 위치부터 보여줘
는 컨텍스트를 쓰면서
와 같은 순서로 진행될 수 있었다.
완성하고 모~든 리스트 사용하는 곳에 해당 로직을 적용시켰다.
이 값을 언제는 초기화시켜주고 언제는 다시 업데이트하고 하는 걸 A 컴포넌트 뿐 아니라 B에서도 해줘야하고, C에서도 해줘야하는 상황이 있는 것들이 조금 헷갈렸던 것 같다.
그럼에도 다행히 원하는 형태로 구현이 잘 돼, 사용하는 내부 직원 모두 만족하는 모습을 보며 뿌듯함을 느낄 수 있었다.😌😌😌