React 특정 컴포넌트 프린트하기(한 장/여러 장)

ChaeChae·2022년 11월 11일
6

회사에서 개발한 기능 중 환자의 리포트를 프린트하는 기능이 있었다. 전체 화면이 아니라 리포트 부분만 프린트해야 해서 ReactToPrint 라이브러리를 사용했다. ReactToPrint는 React 프로젝트에서 컴포넌트 단위로 손쉽게 프린트할 수 있는 기능을 제공한다. 사용 방법도 직관적이고 프린트할 때 적용할 수 있는 다양한 옵션을 가지고 있다.

ReactToPrint GitHub: https://github.com/gregnb/react-to-print

예시 코드 전체 보기: https://codesandbox.io/s/react-to-print-dwm35b

ReactToPrint 기본 사용 방법

기본적인 사용 방법은 아래 코드와 같이 프린트 버튼 자리에 ReactToPrint 컴포넌트를 사용하고 trigger props에 프린트 버튼을 넣으면 된다. 그리고 프린트할 컴포넌트를 ref로 참조하고 content props에 ref.current를 넣어주면 된다. 만약 프린트 대상이 하위 컴포넌트로 분리되어 있다면 상위 컴포넌트에서 ref를 전달하고 하위 컴포넌트에서 forwardRef로 받으면 된다.

혹은 useReactToPrint라는 hook을 써서 표현하는 방법도 있는데, 문서를 보고 둘 중 편한 방식을 선택하면 된다.

App.js

import { useRef, useEffect, useState } from "react";
import ReactToPrint from "react-to-print";
import Report from "./Report";
import "./styles.css";

const App = () => {
  const [content, setContent] = useState("");
  const ref = useRef();

  useEffect(() => {
    fetch("https://baconipsum.com/api/?type=all-meat&start-with-lorem=1")
      .then((res) => res.json())
      .then((data) => setContent(data));
  }, []);

  return (
    <div className="App">
      <div className="header">
        <h2>리포트 보기</h2>
        <ReactToPrint
          trigger={() => <button>리포트 출력</button>}
          content={() => ref.current}
        />
      </div>
      <div>
        <Report ref={ref} content={content} />
      </div>
    </div>
  );
}

export default App;

Report.js

import { forwardRef } from "react";

const Report = forwardRef((props, ref) => {
  return (
    <section ref={ref} className="report">
      <h3>홍길동 님의 재활운동 결과 리포트</h3>
      <p>{props.content}</p>
    </section>
  );
});

export default Report;

styles.css

프린트될 페이지 관련 스타일을 설정하려면 CSS에서 @page를 사용하면 된다. 아래와 같이 종이 사이즈와 여백을 지정해주면 깔끔하게 출력할 수 있다.

@page {
  size: A4;
  margin: 20mm;
}

여러 장 프린트하기

리포트 부분의 기획이 바뀌면서 여러 개의 리포트를 선택하여 출력할 수 있도록 기능을 업데이트해야 했다. 여러 장을 프린트하려면 아래와 같이 프린트할 영역 전체를 감싸고 있는 상위 element에 ref를 넣으면 된다.

<div ref={ref}>
  {reportList.map((report) => (
    <Report key={report.slice(0, 20)} report={report} />
  ))}
</div>

그리고 한 페이지에 하나의 리포트가 깔끔하게 출력되게 하려면 CSS를 손봐야 한다. 한 페이지 단위가 되는 리포트 element에 break-after: page 속성을 주면 해당 element 뒤에 여백이 남아도 다음 페이지에 출력된다. 그리고 화면에 렌더링할 때는 리포트 사이에 margin을 주어 간격을 띄워놓지만, 프린트할 때는 이미 page break를 걸어놓은 상태라 margin을 출력할 필요가 없다. 따라서 @media print를 사용하여 프린트 시에는 margin이 0이 되도록 설정한다.

.report {
  break-after: page;
}

.report + .report {
  margin-top: 40px;
}

@media print {
  .report + .report {
    margin-top: 0;
  }
}

@page {
  size: A4;
  margin: 20mm;
}

다크 모드에서 프린트하기

이렇게 프린트 기능은 완성했지만 한 가지 문제가 남아있었다. 회사 서비스가 기본적으로 다크 테마를 사용해서 프린트하면 검은 배경에 흰 글씨로 나온다는 것이었다. 따라서 프린트할 때는 라이트 테마를 사용하고 프린트가 끝나면 이전 테마로 되돌리는 작업이 필요했다.

우선 ReactToPrintonBeforeGetContent 콜백을 사용하여 프린트 버튼을 누른 후 프린트 대화 상자가 열리기 전에 테마를 변경하도록 했다. 이전 상태를 나타내는 wasDarkMode에 현재 다크 모드 여부를 저장하고, 현재 테마를 라이트 모드로 바꿔주었다. 하지만 프린트 창이 거의 즉시 뜨기 때문에 테마 변경이 반영되지 않았다. 그래서 setTimeout으로 아주 짧은 시간 딜레이를 주고 Promise를 리턴하는 방식으로 해결했다. 그리고 프린트 대화 상자가 닫힌 후에는 onAfterPrint 콜백을 사용하여 현재 테마를 이전 테마로 되돌려주었다. 이렇게 하여 사용자 경험을 해치지 않고 다크 모드에서 프린트할 때만 라이트 모드로 전환했다 되돌리는 로직을 구현했다.

const [isDarkMode, setIsDarkMode] = useState(true);
const [wasDarkMode, setWasDarkMode] = useState(true);

const onBeforeGetContent = () => {
  setWasDarkMode(isDarkMode);
  setIsDarkMode(false);

  return new Promise((resolve) => {
    setTimeout(resolve, 10);
  });
};

const onAfterPrint = () => setIsDarkMode(wasDarkMode);
<ReactToPrint
  trigger={() => <button>리포트 출력</button>}
  content={() => ref.current}
  onBeforeGetContent={onBeforeGetContent}
  onAfterPrint={onAfterPrint}
/>
profile
정리 장인 && FE 개발자

1개의 댓글

comment-user-thumbnail
2023년 8월 7일

도움 많이 됐습니다. 감사합니다

답글 달기