목표 : 사이트 소개 툴팁 구현하기
dor.gg 사이트의 유저들에게 사이트 사용 방법을 설명하는 툴팁을 만드는 작업이었다.
기본적으로는 useState
를 사용하되, 다음번에 유저가 들어왔을 때에는 툴팁이 뜨지 않도록 localStorage
에 저장하는 방법으로 구현 중이었다.
다음은 개략적인 구현 코드이다.
// useTooltipBubble
const useTooltipBubble = () => {
const currentUser = useContext(CurrentUserContext);
const userIndex = currentUser?.user.id;
// 소개 말풍선 이전의 clickable
const [triggerIcon, setTriggerIcon] = useState(
Object.keys(tooltips).reduce(
(prev, curr) => ({ ...prev, [curr]: false }),
{},
),
);
// 소개 말풍선 open
const [bubbleOpen, setBubbleOpen] = useState(
Object.keys(tooltips).reduce(
(prev, curr) => ({ ...prev, [curr]: false }),
{},
),
);
// 소개 말풍선 안 본거 담아둠.
useEffect(() => {
const siteIntroductionListForNewUsers = Object.keys(tooltips);
const siteIntroductionListInStorage = localStorage.getItem(
'1-site-introduction-list' + userIndex,
);
const siteIntroductionList = JSON.parse(siteIntroductionListInStorage);
console.log('useEffect');
if (siteIntroductionListInStorage) {
console.log('사이트 접속한 유저');
if (siteIntroductionList.length > 0) {
// 전체 안 보기했을 때 대비
// 안 본것만 true로 돌림.
siteIntroductionList.forEach(item => {
setTriggerIcon(prev => ({ ...prev, [item]: true }));
});
}
} else {
console.log('처음 방문');
// 로그인한 유저만
if (userIndex) {
localStorage.setItem(
'1-site-introduction-list' + userIndex,
JSON.stringify(siteIntroductionListForNewUsers),
);
// 전체 true로 돌림
siteIntroductionListForNewUsers.forEach(item => {
setTriggerIcon(prev => ({ ...prev, [item]: true }));
});
}
}
}, []);
// clickable 클릭해서 말풍선띄우기
const handleOpenBubble = useCallback(target => {
setTriggerIcon(prev => ({ ...prev, [target]: !prev[target] }));
setBubbleOpen(prev => ({ ...prev, [target]: !prev[target] }));
}, []);
// 말풍선 닫기만 하기
const handleCloseBubble = useCallback(target => {
setTriggerIcon(prev => ({ ...prev, [target]: !prev[target] }));
setBubbleOpen(prev => ({ ...prev, [target]: !prev[target] }));
}, []);
// 해당 말풍선 check (안보이게)
const handleCheckBubble = useCallback(
target => {
const siteIntroductionList = JSON.parse(
localStorage.getItem('1-site-introduction-list' + userIndex),
);
// 해당 trigger, bubble 꺼주기
setTriggerIcon(prev => ({ ...prev, [target]: false }));
setBubbleOpen(prev => ({ ...prev, [target]: false }));
// 그것만 빼서 다시 로컬스토리지 저장
const changedList = siteIntroductionList.filter(item => item !== target);
localStorage.setItem(
'1-site-introduction-list' + userIndex,
JSON.stringify(changedList),
);
},
[userIndex],
);
// 전체 말풍선 check
const handleCheckAllBubble = useCallback(() => {
// 로컬 스토리지에 저장
localStorage.setItem(
'1-site-introduction-list' + userIndex,
JSON.stringify([]),
);
// trigger 다 꺼주고, bubble도 다 꺼줌
setTriggerIcon(prev => ({
...Object.keys(prev).reduce((acc, key) => {
return { ...acc, [key]: false };
}, {}),
}));
setBubbleOpen(prev => ({
...Object.keys(prev).reduce((acc, key) => {
return { ...acc, [key]: false };
}, {}),
}));
}, [userIndex]);
return {
triggerIcon,
bubbleOpen,
handleOpenBubble,
handleCloseBubble,
handleCheckBubble,
handleCheckAllBubble,
};
};
export default useTooltipBubble;
bubble trigger와 bubble 이렇게 두개의 state를 통해서 조절했으며, useEffect
를 통해서 로컬 스토리지에 저장된 값을 가져와서 useState
에 담아주었다.
handleOpenBubble
과 handleCloseBubble
함수를 사용해서 bubble을 열고 닫았으며,
handleCheckBubble
과 handleCheckkAllBubble
함수를 사용해서 bubble을 닫고 다시 켜지지 않도록 로컬스토리지에 저장했다.
문제는 handleCheckAllBubble
함수를 작동하는데에서 발생했다. 분명 함수가 작동하고, 로컬 스토리지의 값도 변하는 것을 확인했는데, 다른 물음표들이 닫히지 않는 것이었다.
처음에는 로컬스토리지의 값이 다시 반영이 되어서 그런가하고 useEffect
에 log를 찍어보고 했는데, 전혀 작동하지 않았다.
서칭을 해보다가 문제를 찾았는데, 이유는 custom hooks
에서 각각의 state를 사용하던 것이었다.
생각해보면 당연한 문제였는데, useTooltipBubble
을 각 useTooltipManager
컴포넌트에서 쓰고 있었고, 각 useTootipManager
는 각 물음표가 필요한 곳에 하나씩 들어가있었다.
그렇게 되니 각 물음표에 10개의 key가 들어있는 각각의 state
가 들어가게 된 것이다.
즉, 상태 관리가 하나도 되지 않고 있던 것이었다.
위의 그림처럼 custom hook에 담아둔 useState
를 통해 전역적인 상태 관리가 되리라고 생각했었으나, 실제로는 각각의 컴포넌트에 개별적인 useState
가 생겨난 꼴이 되어버렸다.
상태 관리를 해주었다.
redux-persist
를 사용할까 했으나 간단하게 context API
를 사용해주었다.
// tooltips context
import React, { createContext, useState } from 'react';
import tooltips from 'lib/static/tooltips';
export const TooltipsContext = createContext(null);
export const TooltipsContextProvider = ({ children }) => {
// 소개 말풍선 이전의 clickable
const [triggerIcon, setTriggerIcon] = useState(
Object.keys(tooltips).reduce(
(prev, curr) => ({ ...prev, [curr]: false }),
{},
),
);
// 소개 말풍선 open
const [bubbleOpen, setBubbleOpen] = useState(
Object.keys(tooltips).reduce(
(prev, curr) => ({ ...prev, [curr]: false }),
{},
),
);
return (
<TooltipsContext.Provider
value={{
trigger: [triggerIcon, setTriggerIcon],
bubble: [bubbleOpen, setBubbleOpen],
}}
>
{children}
</TooltipsContext.Provider>
);
};
export default TooltipsContextProvider;
App
에 provider를 씌어주고
맨위의 useTooltipsBibble.js
에 다음 코드를 변경해주었다.
// state -> context
const { trigger, bubble } = useContext(TooltipsContext);
const [triggerIcon, setTriggerIcon] = trigger;
const [bubbleOpen, setBubbleOpen] = bubble;
문제 해결!