react-pageflip을 활용해 분기점이 있는 책 렌더링하기

NARARIA03·2024년 11월 6일

react-pageflip

목록 보기
2/2

개요

이전 포스트에서는 단순히 책을 렌더링하고, 좌측에는 이미지 우측에는 텍스트를 배치하는데 그쳤다면
이번 포스트에서는 본래 프로젝트의 핵심 기능인 트리 구조 책 렌더링을 구현하며 거친 시행착오를 다룰 것이다.

동화책 데이터를 다시 살펴보자.

[
    {
      "page": 1,
      "content": "...",
      "nextPage": [2],
      "prevPage": 0
    },
    {
      "page": 2,
      "content": "...",
      "nextPage": [3],
      "prevPage": 1
    },

    ...
    
    {
      "page": 7,
      "content": "...",
      "nextPage": [8, 9],
      "prevPage": 6
    },
    {
      "page": 8,
      "choice": "Explore the Secret Room",
      "content": "...",
      "nextPage": [10, 11],
      "prevPage": 7
    },
    {
      "page": 9,
      "choice": "Follow Luna to the Cheese Room",
      "content": "...",
      "nextPage": [12, 13],
      "prevPage": 7
    },
    {
      "page": 10,
      "choice": "Open the Secret Door",
      "content": "...",
      "nextPage": [14],
      "prevPage": 8
    },

    ...
    
  ]

1페이지부터 7페이지까지는 연결 리스트 형태로 이어지다가, 7페이지에서 [8, 9] 페이지로 분기가 발생하고,
8페이지는 [10, 11], 9페이지는 [12, 13] 페이지로 분기가 발생하는 구조다.

이후부터는 각 분기에서 연결 리스트 형태로 다시 이어지게 되나, 페이지 인덱스가 4씩 증가하는 형태가 되게 된다.

예를 들어 왼쪽 선택지만 계속해서 선택한다면, page 번호는 아래와 같다.
1 -> ... -> 7 -> 8 -> 10 -> 14 -> 18

그리고, 위 데이터의 배열 내 하나의 원소 객체를 curPage라는 state로 관리하고 있으며, prevBtnHandlernextBtnHandlercurPage를 변경시킨다.

마지막으로 이전 포스트에서 onFlip 함수를 사용해 prevBtnHandlernextBtnHandler를 호출했으며, 이 함수를 onFlip 이벤트 리스너에 등록시켰었다.

하지만 선택지 컴포넌트가 열렸다가 닫히면, curPage는 부모 컴포넌트에서 props로 받아오기 때문에 업데이트가 잘 되지만, HTMLFlipBook 컴포넌트는 리렌더링 되면서 1페이지로 돌아가게 되는 문제가 발생한다.
또, HTMLFlipBook 컴포넌트는 기본적으로 자식들을 책의 페이지로 사용해 이전 / 다음 페이지로 넘기는 기능을 수행하기 위한 라이브러리라서 트리 구조의 책을 렌더링하기 위해서는 제공되는 함수들을 추가로 활용해야 한다.


HTMLFlipBook이 리렌더링 되어도 curPage부터 렌더링 되도록 하기

다행이도, react-pageflip 라이브러리에서 HTMLFlipBook 컴포넌트의 propsstartPage를 제공한다.

또 지금 데이터의 page1부터 시작해서 1씩 증가하는 반면, HTMLFlipBook 컴포넌트 안의 데이터는 이미지와 텍스트가 찢어져있어 2씩 증가하며 0부터 시작하므로, 정확한 startPage를 계산하려면 curPage.page * 2 - 2를 해줘야 한다.

<HTMLFlipBook
  width={400}
  height={400}
  showCover={false}
  startZIndex={30}
  onFlip={onFlipHandler}
  startPage={curPage.page * 2 - 2} // 시작 페이지: 트리구조 인덱스에 맞게 조절
  >
  {(() => {
    const pages = [];
    for (let i = 0; i < storyBooks.length; i += 1) {
      const storyBook = storyBooks[i];
      pages.push(<Image key={`${i}-image`} image={storyBook.image} />);
      pages.push(<Text key={`${i}-text`} text={storyBook.content} />);
    }
    return pages;
  })()}
</HTMLFlipBook>

위 그림에서 P 뒤의 숫자는 데이터에서 page 값이고, 빨간색 숫자는 HTMLFlipBook에서 startPage 값으로 사용해야 하는 값이다. 이 관계를 보면 수식이 이해가 잘 될 것이다.

startPage props를 추가한 결과, 선택지에서 돌아오면, curPage부터 책이 다시 시작되는 것을 알 수 있다.

하지만, 1 -> ... -> 7 -> 8 -> 10 -> 14 -> 18 에서 10 -> 14 -> 18 부분이 아직 해결되지 않았다. 현재는 여러 분기가 존재하는 책이 아니다!

분기를 구현하기 위해서는 다음 / 이전 페이지로 이동할 때 단순히 다음 / 이전 요소를 렌더링 하는 대신, 데이터의 nextPage / prevPage 값을 활용해서 렌더링 해야만 한다.


데이터의 트리 구조대로 책이 보이도록 구현

구현해야 하는 것은 명확하다.
다음 / 이전 페이지로 이동할 때마다 curPagenextPage / prevPage 데이터를 활용해서 해당 페이지로 점프시키되, 애니메이션은 적용하는 것이다.

이를 위해 먼저 HTMLFlipBook 컴포넌트의 기본 페이지 이동 기능을 전부 꺼줘야 한다.

<HTMLFlipBook
  width={400}
  height={400}
  showCover={false}
  showPageCorners={false} // 페이지 구석에 커서를 가져다대면 다음 장을 넘기는 듯한 모션 제거
  startZIndex={30}
  useMouseEvents={false} // 기본 클릭 이벤트 비활성화
  disableFlipByClick={true} // 클릭하면 플립 이벤트 발생하던거 비활성화
  startPage={curPage.page * 2 - 2} // 시작 페이지: 트리구조 인덱스에 맞게 조절
  onFlip={onFlipHandler}
  >
 ...
</HTMLFlipBook>

이렇게 해주면 기본적으로 왼쪽 페이지를 클릭하면 이전 페이지로 이동하고, 오른쪽 페이지를 클릭하면 다음 페이지로 이동하는 기능이 동작하지 않게 된다.

그 다음, HTMLFlipBook 컴포넌트의 자식으로 전달하는 ImageText 컴포넌트에게 onClick 이벤트 리스너를 전달할 것이다.

const Image = React.forwardRef(
  (
    props: { image: string; onClick: () => void },
    ref: LegacyRef<HTMLDivElement>
  ) => {
    return (
      <div ref={ref} data-density="hard">
        <div
          onClick={props.onClick}
          className="w-full h-full p-8"
          style={{
            backgroundImage: "url('/img/paper.jpg')",
            backgroundSize: "cover",
          }}
        >
          <img src={props.image} alt="storybook" />
        </div>
      </div>
    );
  }
);

const Text = React.forwardRef(
  (
    props: { text: string; page: string; onClick: () => void },
    ref: LegacyRef<HTMLDivElement>
  ) => {
    return (
      <div ref={ref} data-density="hard">
        <div
          onClick={props.onClick}
          className="w-full h-full"
          style={{
            backgroundImage: "url('/img/paper.jpg')",
            backgroundSize: "cover",
          }}
        >
          <p>{props.page}</p>
          <p>{props.text}</p>
        </div>
      </div>
    );
  }
);

참고로, 두 컴포넌트의 최상위 div에 적용된 data-density="hard"는 책의 커버처럼 딱딱한 재질로 설정한다는 의미이다. 기존에는 종이처럼 말리면서 넘어갔다면, 이 옵션을 주면 딱딱한 책장처럼 넘어가게 된다. 디자인적인 요소! github issue

이제, 이 두 컴포넌트에 onClick 이벤트 핸들러를 연결해줘야 하는데, 이 핸들 함수는 상위 컴포넌트에서 받아오도록 구현할 것이다. 이전 포스트에서 onFlip 함수에 활용했던 함수와 거의 그대로라고 보면 된다!

그리고 onFlip 이벤트 리스너와 핸들 함수를 제거한다. (onFlip 이벤트는 flip 이벤트가 발생 후 종료되는 시점에 호출되기 때문에, 페이지를 다 넘긴 뒤에 책 내용이 달라지는 문제가 있다)

function BookPageComponent({
  curPage,
  prevBtnHandler, // 핸들러를 props로 받아옴
  nextBtnHandler, // 핸들러를 props로 받아옴
  storyBooks,
}: BookPageProps): JSX.Element {
  ...
  return (
    <div className="w-full h-full relative px-14 pt-7 pb-[2rem] flex flex-col
                    justify-center items-center">
      ...
      <HTMLFlipBook
        width={400}
        height={400}
        showCover={false}
        showPageCorners={false}
        startZIndex={30}
        useMouseEvents={false} // 기본 클릭 이벤트 비활성화
        disableFlipByClick={true} // 클릭하면 플립 이벤트 발생하던거 비활성화
        startPage={curPage.page * 2 - 2} // 시작 페이지: 트리구조 인덱스에 맞게 조절     
        {(() => {
          const pages = [];
          for (let i = 0; i < storyBooks.length; i += 1) {
            const storyBook = storyBooks[i];

            pages.push(
              <Image
                onClick={prevBtnHandler} // 핸들러 연결
                key={`${i}-image`}
                image={storyBook.image}
              />
            );
            pages.push(
              <Text
                onClick={nextBtnHandler} // 핸들러 연결
                key={`${i}-text`}
                page={storyBook.page.toString()}
                text={storyBook.content}
              />
            );
          }
          return pages;
        })()}
      </HTMLFlipBook>
    </div>
  );
}

export default BookPageComponent;

그리고, 부모 컴포넌트에서 prevBtnHandlernextBtnHandler를 전달해줘야 하는데, 이전 포스트에서 수정한 내역을 다시 복구한 채로 넘겨줄 것이다. (onFlip을 사용하지 않기 때문!)

const prevBtnHandler = () => {
    if (curPage.page > 1) {
      const prevPageData = storyBookData.data.find(
        (page) => page.page === curPage.prevPage
      );
      if (prevPageData) {
        setCurPage(prevPageData);
        setEndStoryFlag(false);
      }
    }
  };

  const nextBtnHandler = () => {
    if (curPage.nextPage.length === 1) {
      const nextPageData = storyBookData.data.find(
        (page) => curPage.nextPage[0] === page.page
      );
      if (nextPageData) {
        setSelectFlag(false);
        setCurPage(nextPageData);
      }
    } else if (curPage.nextPage.length === 2) {
      const nextPageCase1 = storyBookData.data.find(
        (page) => curPage.nextPage[0] === page.page
      );
      const nextPageCase2 = storyBookData.data.find(
        (page) => curPage.nextPage[1] === page.page
      );
      const pageList = [nextPageCase1, nextPageCase2];
      setSelectPage(pageList);
      setSelectFlag(true);
    } else if (curPage.nextPage.length === 0) {
      setEndStoryFlag(true);
    }
  };

로직이 달라진 것은 아니다. 달라진 점은 매개변수가 사라졌다는 점이다. 기존에는 onFlip 함수에서 next 버튼이 눌렸는지 prev 버튼이 눌렸는지를 체크한 뒤, 각각의 핸들러를 호출했기 때문에 매개변수가 필요했다.
하지만 지금은 prevBtnHandlernextBtnHandler가 각각의 페이지 onClick 이벤트에 연결되어 있기 때문에 매개변수가 필요없다.

하지만, 이렇게 하면 onClick 핸들러 덕분에 curPage state는 변경되지만, HTMLFlipBook 컴포넌트 내에서 페이지가 변경되지 않는다. 사실 당연한게, HTMLFlipBook 컴포넌트는 curPage와 아무 관계가 없기 때문이다.

그럼.. 이제 HTMLFlipBookcurPage 간에 관계를 만들어야 한다..!

공식 문서 Methods

공식 문서에 따르면, ref를 사용해 PageFlip object를 가져오면, 메서드들을 사용할 수 있다고 한다. (단, TS 지원은 미흡하다. 필자는 해당 컴포넌트에서 발생하는 TS 에러는 모두 무시했다)

그러면, useRef를 통해 PageFlip 오브젝트를 가져오고, 공식 문서에 설명되어 있는 flip 함수를 활용해서 구현할 수 있지 않을까? 바로 해보자.

function BookPageComponent({
  curPage,
  prevBtnHandler,
  nextBtnHandler,
  storyBooks,
}: BookPageProps): JSX.Element {
  const bookRef = useRef(); // useRef 생성

  // useEffect를 사용해 curPage가 변경될 때마다 flip 함수를 호출해서 curPage에 해당하는 페이지로 flip 시킴
  useEffect(() => {
    if (bookRef?.current?.pageFlip()?.flip) {
      console.log(`curPage: ${curPage.page}`);
      bookRef.current.pageFlip().flip(curPage.page * 2 - 2);
    }
  }, [curPage]);

  return (
    <div className="w-full h-full relative px-14 pt-7 pb-[2rem] flex flex-col
                    justify-center items-center">
      <HTMLFlipBook
        width={400}
        height={400}
        showCover={false}
        showPageCorners={false}
        startZIndex={30}
        useMouseEvents={false} // 기본 클릭 이벤트 비활성화
        disableFlipByClick={true} // 클릭하면 플립 이벤트 발생하던거 비활성화
        startPage={curPage.page * 2 - 2} // 시작 페이지: 트리구조 인덱스에 맞게 조절
        ref={bookRef} // ref 연결
      >
        {(() => {
          const pages = [];
          for (let i = 0; i < storyBooks.length; i += 1) {
            const storyBook = storyBooks[i];

            pages.push(
              <Image
                onClick={prevBtnHandler}
                key={`${i}-image`}
                image={storyBook.image}
              />
            );
            pages.push(
              <Text
                onClick={nextBtnHandler}
                key={`${i}-text`}
                page={storyBook.page.toString()}
                text={storyBook.content}
              />
            );
          }
          return pages;
        })()}
      </HTMLFlipBook>
    </div>
  );
}

export default BookPageComponent;

이제 로직의 흐름은 아래와 같다.

  1. 왼쪽 / 오른쪽 페이지 클릭
  2. prevBtnHandler / nextBtnHandler 실행
  3. curPage 상태 업데이트
  4. useEffect가 재실행 되어 flip 함수를 사용해 curPage에 맞는 페이지로 책을 넘김

사실 여기까지 하면 정상적으로 동작할 줄 알았으나... 10 -> 14로 이동해야 하는데 10 -> 14 -> 11 페이지로 이동하는 문제가 발생한다...

이 문제를 해결하려고 onFlip 이벤트의 index, curPage의 page 값, onChange 이벤트까지 거의 모든 영역에 콘솔을 찍어보고, GPT-o1(신기술! 도움 안 됨)에게 질문 하는 등 하루동안 삽질을 엄청 했다.

flip 함수 자체의 문제인가 의심도 했었는데, flip()과 동일한 기능을 수행하지만 애니메이션만 없는 함수인 turnToPage()를 사용하면 위 문제가 발생하지 않았기 때문에 많이 의심스러웠다.

하지만 정말 어이없이 해결하게 됐는데, 그냥 onFlip 이벤트 리스너를 건드려보다 우연히 해결해버렸다...
아직 왜 해결되었는지 잘 이해하지는 못했다. 해결 방법은, onFlip 이벤트 리스너 내부에서 flip()을 다시 호출해주면 된다..

<HTMLFlipBook
  width={400}
  height={400}
  showCover={false}
  showPageCorners={false}
  startZIndex={30}
  useMouseEvents={false}
  disableFlipByClick={true}
  startPage={curPage.page * 2 - 2}
  ref={bookRef}
  onFlip={() => {
    bookRef?.current?.pageFlip()?.flip(curPage.page * 2 - 2);
  }} // 이 부분 추가!
  >
  ...
</HTMLFlipBook>

다만 예상하기로는 flip()의 실행이 끝나면 onFlip 이벤트가 발생하고, 이 이벤트가 비어있으면 내부적으로 다음 페이지로 이동시키는 건가 싶다.


이벤트 리스너 중복 처리 방지하기

다행히 큰 산은 넘었지만, 페이지를 빠르게 여러 번 클릭하면 curPageHTMLFlipBook의 페이지가 틀어지는 문제가 발생한다. 이는 flip()이 실행 중일 때 curPage가 변경되어 발생하는 문제였고, 공식 문서에 현재 flip 중인지를 받아올 수 있는 방법이 소개되어 있어 이를 활용해 간단하게 해결했다.

공식 문서

이벤트 리스너 중, onChangeState를 활용하면 flippingread를 식별 가능하다.
flip()이 실행 중일 때는 flipping 상태가 되고, 끝나면 read 상태가 되므로 상태로 받아와 관리하면서 if문으로 체크만 해주면 되겠다!

function BookPageComponent({
  curPage,
  prevBtnHandler,
  nextBtnHandler,
  storyBooks,
}: BookPageProps): JSX.Element {
  const bookRef = useRef();
  const [bookChange, setBookChange] = useState<string>(""); // state 생성

  useEffect(() => {
    if (bookRef?.current?.pageFlip()?.flip) {
      // 조건문 추가!
      if (bookChange !== "flipping") {
        bookRef.current.pageFlip().flip(curPage.page * 2 - 2);
      }
    }
  }, [bookChange, curPage]);

  return (
    <div className="w-full h-full relative px-14 pt-7 pb-[2rem] flex flex-col
                    justify-center items-center">
      <HTMLFlipBook
        width={400}
        height={400}
        showCover={false}
        showPageCorners={false}
        startZIndex={30}
        useMouseEvents={false}
        disableFlipByClick={true}
        startPage={curPage.page * 2 - 2}
        ref={bookRef}
        onFlip={() => {
          bookRef?.current?.pageFlip()?.flip(curPage.page * 2 - 2);
        }}
        onChangeState={(e) => setBookChange(e.data)}
        >
        {(() => {
          const pages = [];
          for (let i = 0; i < storyBooks.length; i += 1) {
            const storyBook = storyBooks[i];

            pages.push(
              <Image
                // 이벤트 핸들러에 조건문 추가!
                onClick={() => {
                  if (bookChange !== "flipping") {
                    prevBtnHandler();
                  }
                }}
                key={`${i}-image`}
                image={storyBook.image}
              />
            );
            pages.push(
              <Text
                // 이벤트 핸들러에 조건문 추가!
                onClick={() => {
                  if (bookChange !== "flipping") {
                    nextBtnHandler();
                  }
                }}
                key={`${i}-text`}
                page={storyBook.page.toString()}
                text={storyBook.content}
              />
            );
          }
          return pages;
        })()}
      </HTMLFlipBook>
    </div>
  );
}

export default BookPageComponent;

여기까지 처리하면, 드디어 맨 위 gif처럼 드디어 구현할 수 있다...!


결론

잘 모르는 라이브러리에서 버그를 만나면 항상 두렵지만, 이를 해결하는 것 만큼 짜릿한 것도 없는 것 같다.
만약 해당 라이브러리로 나와 유사한 기능을 구현하려고 하는 개발자라면, 도움을 받았으면 한다!

포스트에서 다룬 두 컴포넌트의 전체 코드와 간단한 주석을 공유하면서 마치겠다.

// BookContents.tsx
interface BookContentsProps {
  storyBookData: StoryBook; // 포스트에서 다룬 하나의 동화책 데이터를 의미합니다.
}

export default function BookContents({
  storyBookData,
}: BookContentsProps): JSX.Element {
  const [curPage, setCurPage] = useState<StoryBookData>(storyBookData.data[0]);
  // 선택지에서 고르게 될 두 개의 페이지를 관리하는 state
  const [selectPage, setSelectPage] = useState<(StoryBookData | undefined)[]>();
  // 선택지 컴포넌트 렌더링 유무
  const [selectFlag, setSelectFlag] = useState<boolean>(false); 
  // 동화가 끝났는지 유무
  const [endStoryFlag, setEndStoryFlag] = useState<boolean>(false); 

  const darknessClass = endStoryFlag ? "brightness-50" : ""; // 조건부 스타일링용

  // 왼쪽 페이지를 클릭하면 실행될 핸들러
  const prevBtnHandler = () => {
    if (curPage.page > 1) {
      const prevPageData = storyBookData.data.find(
        (page) => page.page === curPage.prevPage
      );
      if (prevPageData) {
        setCurPage(prevPageData);
        setEndStoryFlag(false);
      }
    }
  };

  // 오른쪽 페이지를 클릭하면 실행될 핸들러
  const nextBtnHandler = () => {
    if (curPage.nextPage.length === 1) {
      const nextPageData = storyBookData.data.find(
        (page) => curPage.nextPage[0] === page.page
      );
      if (nextPageData) {
        setSelectFlag(false);
        setCurPage(nextPageData);
      }
    } else if (curPage.nextPage.length === 2) {
      const nextPageCase1 = storyBookData.data.find(
        (page) => curPage.nextPage[0] === page.page
      );
      const nextPageCase2 = storyBookData.data.find(
        (page) => curPage.nextPage[1] === page.page
      );
      const pageList = [nextPageCase1, nextPageCase2];
      setSelectPage(pageList);
      setSelectFlag(true);
    } else if (curPage.nextPage.length === 0) {
      setEndStoryFlag(true);
    }
  };

  // 선택지에 도달하면 선택지 컴포넌트를 렌더링
  if (selectFlag && selectPage) {
    return (
      <div>
        <BookBackground selectFlag={selectFlag} />
        <SelectPageComponent
          selectPage={selectPage}
          setCurPage={setCurPage}
          setSelectFlag={setSelectFlag}
        />
      </div>
    );
  }

  // 선택지가 아니면 HTMLFlipBook이 포함된 컴포넌트를 렌더링
  return (
    <>
      {endStoryFlag && <EndPopupComponent />}
      <div className={`w-full h-full ${darknessClass}`}>
        <BookPageComponent
          curPage={curPage}
          prevBtnHandler={prevBtnHandler}
          nextBtnHandler={nextBtnHandler}
          storyBooks={storyBookData.data}
        />
      </div>
    </>
  );
}
// BookPageComponent.tsx
const Image = React.forwardRef(
  (
    props: { image: string; onClick: () => void },
    ref: LegacyRef<HTMLDivElement>
  ) => {
    return (
      <div ref={ref} data-density="hard">
        <div
          onClick={props.onClick}
          className="w-full h-full p-8"
          style={{
            backgroundImage: "url('/img/paper.jpg')",
            backgroundSize: "cover",
          }}
        >
          <img src={props.image} alt="storybook" />
        </div>
      </div>
    );
  }
);

const Text = React.forwardRef(
  (
    props: { text: string; page: string; onClick: () => void },
    ref: LegacyRef<HTMLDivElement>
  ) => {
    return (
      <div ref={ref} data-density="hard">
        <div
          onClick={props.onClick}
          className="w-full h-full"
          style={{
            backgroundImage: "url('/img/paper.jpg')",
            backgroundSize: "cover",
          }}
        >
          <p>{props.page}</p>
          <p>{props.text}</p>
        </div>
      </div>
    );
  }
);

interface BookPageProps {
  curPage: StoryBookData;
  prevBtnHandler: () => void;
  nextBtnHandler: () => void;
  storyBooks: StoryBookData[];
}

function BookPageComponent({
  curPage,
  prevBtnHandler,
  nextBtnHandler,
  storyBooks,
}: BookPageProps): JSX.Element {
  const [bookChange, setBookChange] = useState<string>("");
  const bookRef = useRef();

  useEffect(() => {
    if (bookRef?.current?.pageFlip()?.flip) {
      console.log(`curPage: ${curPage.page}`);
      if (bookChange !== "flipping") {
        bookRef.current.pageFlip().flip(curPage.page * 2 - 2);
      }
    }
  }, [bookChange, curPage]);

  return (
    <div className="w-full h-full relative px-14 pt-7 pb-[2rem] flex flex-col
                    justify-center items-center">
      <HTMLFlipBook
        width={400}
        height={400}
        showCover={false}
        showPageCorners={false}
        startZIndex={30}
        useMouseEvents={false}
        disableFlipByClick={true} 
        startPage={curPage.page * 2 - 2} 
        ref={bookRef}
        onFlip={() => {
          bookRef?.current?.pageFlip()?.flip(curPage.page * 2 - 2);
        }}
        onChangeState={(e) => setBookChange(e.data)}
      >
        {(() => {
          const pages = [];
          for (let i = 0; i < storyBooks.length; i += 1) {
            const storyBook = storyBooks[i];

            pages.push(
              <Image
                onClick={() => {
                  if (bookChange !== "flipping") {
                    prevBtnHandler();
                  }
                }}
                key={`${i}-image`}
                image={storyBook.image}
              />
            );
            pages.push(
              <Text
                onClick={() => {
                  if (bookChange !== "flipping") {
                    nextBtnHandler();
                  }
                }}
                key={`${i}-text`}
                page={storyBook.page.toString()}
                text={storyBook.content}
              />
            );
          }
          return pages;
        })()}
      </HTMLFlipBook>
    </div>
  );
}

export default BookPageComponent;
profile
신입 프론트엔드 개발자입니다. React와 RN 생태계를 좋아합니다.

0개의 댓글