관제 시스템 프로젝트를 진행하면서 기능, 내용 설명에 대한 온보딩 (= Product tour) 을 추가하기로 하여 react-joyride를 적용하여 사용법에 대해 정리하고자 한다.
react-joyride npm
react-joyride Homepage
react-joyride 설치
npm i react-joyride
추가로 필요한 npm
react-use npm
npm i react-use
기본 사용법
steps 배열 안에 보여줄 내용과 target에 연결되는 영역과 동일한 클래스 명을 부여하면 된다.
다양한 Props 들이 있어 위치나, 스타일 등을 변경 할 수 있다.
handleClickStart : 온보딩 가이드를 시작하는 역할을 한다.
handleJoyrideCallback : 온보딩 가이드가 진행되거나 완료될 때 호출되며 가이드의 상태에 따라 원하는 동작을 수행한다. finishedStatuses 배열은 가이드가 완료(STATUS.FINISHED)되었거나 스킵 상태(STATUS.SKIPPED)를 나타내는 값들을 담고 있어 if 문에서 finishedStatuses에 포함된 상태인 경우에만 setState({ run: false })를 호출하여 run 상태를 false로 업데이트하여 온보딩 가이드를 종료합니다.
import { css } from "@emotion/react";
import { useState } from "react";
// JoyRide
import dynamic from "next/dynamic";
const JoyRide = dynamic(() => import("react-joyride"), { ssr: false });
import { CallBackProps, STATUS } from "react-joyride";
import { useSetState } from "react-use";
export default function Home() {
const commonStepConfig = {
floaterProps: {
disableAnimation: true,
},
spotlightPadding: 0,
showSkipButton: true,
};
const onBoardingCreate = (
content: string,
target: string,
addConfig = {}
) => {
return {
content,
target,
...commonStepConfig,
...addConfig,
};
};
const [{ run, steps }, setState] = useSetState<any>({
run: false,
steps: [
{
content: <h2>시작</h2>,
locale: { skip: <strong aria-label="skip">S-K-I-P</strong> },
placement: "center",
target: "body",
},
onBoardingCreate("정보1", ".info-one"),
onBoardingCreate("정보2", ".info-two"),
onBoardingCreate("정보3", ".info-three"),
],
});
const handleClickStart = (event: React.MouseEvent<HTMLElement>) => {
event.preventDefault();
setState({
run: true,
});
};
const handleJoyrideCallback = (data: CallBackProps) => {
const { status, type } = data;
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
if (finishedStatuses.includes(status)) {
setState({ run: false });
}
};
return (
<>
<section css={HomePage}>
<p
css={Start}
onClick={(event) => {
handleClickStart(event);
}}
>
Start
</p>
<div css={Joy}>
<div className="info-one">info-one</div>
<div className="info-two">info-two</div>
<div className="info-three">info-three</div>
</div>
</section>
{/* 온보딩 창 */}
<JoyRide
callback={handleJoyrideCallback}
continuous
run={run}
scrollToFirstStep
showProgress={true}
showSkipButton
hideBackButton={true}
steps={steps}
hideCloseButton={false}
styles={{
options: {
zIndex: 10000,
overlayColor: "rgba(0, 0, 0, 0.6)",
textColor: "#000",
},
spotlight: {
border: "2px solid #fff",
borderRadius: 8,
},
tooltip: {
borderRadius: 10,
},
tooltipContent: {
fontWeight: 600,
marginTop: 20,
},
}}
/>
</>
);
}
결과
변형
진행한 프로젝트에서는 추가로 궁금한 영역을 클릭하면 해당 온보딩이 뜨는 형식으로 변경하여 진행하였다. 특정 상태에서만 정보를 볼 수 있는 구조로 handleClickStart 에서 각 step 을 받아와 해당 영역 클릭 시 setHelpJoyRideObj를 사용하여 helpJoyRideObj 상태를 업데이트한다.
이어지지 않고 관련 정보만 보이게 했다. 클릭된 도움말 단계(step)에 대한 값을 **true**로 설정하여 해당 도움말을 표시하도록 했다.
전체코드
// helpStore.ts
import { create } from "zustand";
interface HelpState {
showHelp: boolean;
toggleShowHelp: () => void;
}
export const helpStore = create<HelpState>((set) => ({
showHelp: false,
toggleShowHelp: () => set((state) => ({ showHelp: !state.showHelp })),
}));
import { colors } from "@/styles/Color";
import { css } from "@emotion/react";
import { useState, useEffect } from "react";
import { HiX } from "react-icons/hi";
import { helpStore } from "@/stroe/helpStore";
// JoyRide
import dynamic from "next/dynamic";
const JoyRide = dynamic(() => import("react-joyride"), { ssr: false });
import { CallBackProps, STATUS } from "react-joyride";
import { useSetState } from "react-use";
export default function Home() {
//도움말
const commonStepConfig = {
floaterProps: {
disableAnimation: true,
},
spotlightPadding: 0,
showSkipButton: true,
disableBeacon: true,
};
const onBoardingCreate = (
content: string,
target: string,
addConfig = {}
) => {
return {
content,
target,
...commonStepConfig,
...addConfig,
};
};
const [{ run, steps }, setState] = useSetState<any>({
run: false,
steps: [
onBoardingCreate("정보1", ".info-one"),
onBoardingCreate("정보2", ".info-two"),
onBoardingCreate("정보3", ".info-three"),
],
});
const { showHelp, toggleShowHelp } = helpStore();
const [stepNum, setStepNum] = useState<number>(0);
const [helpJoyRideObj, setHelpJoyRideObj] = useState<any>({
1: false,
2: false,
3: false,
});
useEffect(() => {
if (showHelp) {
setHelpJoyRideObj({
1: false,
2: false,
3: false,
});
}
}, [showHelp]);
const handleClickStart = (
event: React.MouseEvent<HTMLElement>,
step: number
) => {
event.preventDefault();
const updateHelpJoyRideObj = (prevObj: any) => {
const updatedObj = { ...prevObj };
updatedObj[step] = true;
return updatedObj;
};
setHelpJoyRideObj((prevObj: any) => updateHelpJoyRideObj(prevObj));
if (showHelp) {
document.body.style.overflow = "hidden";
setStepNum(step);
setState({ run: true });
}
};
const handleJoyrideCallback = (data: CallBackProps) => {
const { status, type } = data;
const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED];
if (finishedStatuses.includes(status)) {
setHelpJoyRideObj({
1: false,
2: false,
3: false,
});
setState({ run: false });
}
// beacon 비활성화
if (data.action === "close" && data.type === "step:after") {
setHelpJoyRideObj({
1: false,
2: false,
3: false,
});
setState({ run: false });
}
};
const helpDarkShow = {
display: showHelp ? "block" : "none",
};
return (
<>
<section>
<p css={Show} onClick={toggleShowHelp}>
Show
</p>
<div css={Joy}>
<div
className="info-one"
onClick={(event) => {
handleClickStart(event, 0);
}}
>
<div
css={[
!showHelp || helpJoyRideObj[0] ? "" : helpDark,
helpDarkShow,
]}
>
info-one
</div>
</div>
<div
className="info-two"
onClick={(event) => {
handleClickStart(event, 1);
}}
>
<div
css={[
!showHelp || helpJoyRideObj[1] ? "" : helpDark,
helpDarkShow,
]}
>
info-two
</div>
</div>
<div
className="info-three"
onClick={(event) => {
handleClickStart(event, 2);
}}
>
<div
css={[
!showHelp || helpJoyRideObj[2] ? "" : helpDark,
helpDarkShow,
]}
>
info-three
</div>
</div>
</div>
</section>
{/* 도움말 창 */}
<JoyRide
callback={handleJoyrideCallback}
continuous
run={run}
scrollToFirstStep
showProgress={false}
showSkipButton={false}
hideBackButton={true}
steps={steps}
hideCloseButton={false}
stepIndex={stepNum}
styles={{
options: {
zIndex: 10000,
overlayColor: "rgba(0, 0, 0, 0.6)",
textColor: "#000",
},
spotlight: {
border: "2px solid #fff",
borderRadius: 8,
},
buttonSkip: {
display: "none",
},
buttonNext: {
display: "none",
},
buttonClose: {
position: "absolute",
top: 0,
right: 10,
fontSize: 22,
},
tooltip: {
borderRadius: 10,
},
tooltipContent: {
fontWeight: 600,
marginTop: 20,
},
}}
/>
{showHelp && (
<div css={Dark}>
<div onClick={toggleShowHelp} className="close">
<HiX />
</div>
</div>
)}
</>
);
}
const Show = css`
padding: 14px;
background-color: #ba132f;
border-radius: 10px;
color: #fff;
cursor: pointer;
`;
const Joy = css`
display: flex;
align-items: center;
gap: 50px;
> div {
position: relative;
width: 200px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
background-color: pink;
border-radius: 10px;
z-index: 50;
> div {
display: flex;
align-items: center;
justify-content: center;
}
}
`;
const helpDark = css`
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 8px;
border: 1px solid rgba(255, 230, 0, 0.9);
cursor: pointer;
z-index: 1;
background-color: rgba(0, 0, 0, 0.6);
`;
const Dark = css`
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.6);
.close {
z-index: 20;
position: absolute;
left: 50%;
top: 0;
padding: 10px;
transform: translateX(-50%);
color: #fff;
font-size: 24px;
cursor: pointer;
}
`;
결과
'React' 카테고리의 다른 글
Pagination (0) | 2024.08.26 |
---|---|
검색 기능 (0) | 2024.08.26 |
React 에서 탭 기능 구현 (0) | 2024.08.26 |
LocalStorage 값 저장, 가져오기, 삭제 (0) | 2024.08.26 |
부모에서 자식 / 자식에서 부모 값 전달하기 (0) | 2024.08.25 |