React

react-joyride

minsun309 2024. 8. 26. 09:05

관제 시스템 프로젝트를 진행하면서 기능, 내용 설명에 대한 온보딩 (= Product tour) 을 추가하기로 하여 react-joyride를 적용하여 사용법에 대해 정리하고자 한다.

 

react-joyride npm

 

react-joyride

Create guided tours for your apps. Latest version: 2.8.2, last published: 3 months ago. Start using react-joyride in your project by running `npm i react-joyride`. There are 215 other projects in the npm registry using react-joyride.

www.npmjs.com

 

react-joyride Homepage

 

Overview | React Joyride

Last updated 4 months ago

docs.react-joyride.com

 

 

React Joyride Demo

 

react-joyride.com

 

react-joyride 설치

npm i react-joyride

 

추가로 필요한 npm

react-use npm

 

react-use

Collection of React Hooks. Latest version: 17.5.1, last published: a month ago. Start using react-use in your project by running `npm i react-use`. There are 2870 other projects in the npm registry using react-use.

www.npmjs.com

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;
  }
`;

 

 

결과