관제 시스템 프로젝트를 진행하면서 기능, 내용 설명에 대한 온보딩 (= 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; } `;
결과