웹에서 클립보드에 텍스트 저장하기 (Clipboard API) with 리액트 + TypeScript

요즘 내 트리를 꾸며줘, 롤링페이퍼처럼 링크 공유형 서비스가 유행 중이다.
나 또한 그러한 서비스인 너가소개서라는 프로젝트에 참여 중인데, 링크를 생성하는 뷰를 맡아 클립보드에 텍스트를 저장하는 기능을 개발하게 되었다.
비슷한 서비스를 개발해보고 싶은 프론트엔드 개발자라면 도움이 될 수도 있는 내용이다!

본론

웹 브라우저에서 클립보드에 텍스트를 저장하게 해주는 API는 Document.execCommand(), Clipboard가 있는데 전자는 deprecated 되었으며 이를 대체하기 위해 후자가 고안되었다.
따라서 이 글에서는 Clipboard API를 이용하는 방법을 소개하겠다.

Clipboard API는 navigator.clipboard를 통해 전역에서 접근할 수 있다.
몇 가지 메소드가 있는데 이곳에서 확인할 수 있고, 우리는 그 중에서 writeText()를 이용할 것이다.
이는 클립보드에 파라미터로 들어온 string 값을 저장해주는 메소드이다.

copyClipboard.ts

export const copyClipboard = async (
  text: string,
  successAction?: () => void,
  failAction?: () => void,
) => {
  try {
    await navigator.clipboard.writeText(text);
    successAction && successAction();
  } catch (error) {
    failAction && failAction();
  }
};

나는 위와 같이 copyClipboard라는 유틸 함수를 만들어 프로젝트 내에서 필요한 곳에 가져다 썼다.

복사할 string 값과 복사가 성공/실패했을 때 실행할 함수를 파라미터로 받는다.
이 함수들은 따로 지정해주지 않아도 된다. (obtional 파라미터)

writeText()는 클립보드의 내용이 업데이트 되기까지의 과정을 Promise를 통해 비동기로 처리한다.
따라서 copyClipboard를 async 함수로 만들어 성공/실패 여부를 처리해주었다.

함수 내부에서 action을 다루지 않고, 요청 성공 여부에 따라 true/false 값을 주는 state를 반환하여 이 값을 통해 외부에서 자유롭게 action을 결정하는 hook(리액트)을 만들어 보았었는데
문제되는 점이 있어 이렇게 수정하였다. 이 내용은 마지막에 다뤄보겠다!

활용 예시

나는 링크 생성 성공/실패 시 토스트로 그 결과를 알려줘야 했기 때문에 action 함수로 서진이가 만든 토스트 함수를 넣어줬다.

import { IcLinkCopy } from '@assets/icons'; // svg 컴포넌트

생략

<StLinkBox>
  <input type="text" value={link} disabled />
  <div></div>
  <IcLinkCopy
  	onClick={() =>
          copyClipboard(
            link,
            () => fireToast({ content: '링크가 클립보드에 저장되었습니다.', bottom: 190 }),
            () => fireToast({ content: '다시 시도해주세요.', bottom: 190 }),
          )
        }
   />
</StLinkBox>

생략

위 코드의 스타일드 컴포넌트, link 값 생성 등에 관련된 내용을 다 올리기에는 너무 코드가 광범위 해서 포함하지 않겠지만, 호옥시 궁금하신 분이 계시다면 댓글 남겨주시길 바란다!

결과

복사가 실제로 돼서 붙여넣기를 하는 것까지는 캡처 못했지만 실제로 복사가 잘 된다!

시도했다가 실패한 방법

위에서 언급했던 것처럼 처음에는 공통으로 사용할 함수는 복사 성공 여부만 알려주고 외부에서 자유롭게 action을 처리할 수 있도록 했다.
이는 isCopy라는 state를 반환하는 hook을 만들어 구현했다.

useCopyClipboard.ts

import { useState } from 'react';

export default function useCopyClipboard(): [
  boolean,
  React.Dispatch<React.SetStateAction<boolean>>,
  (text: string) => Promise<boolean>,
] {
  const [isCopy, setIsCopy] = useState<boolean>(false);
  const copyClipboard = async (text: string) => {
    try {
      await navigator.clipboard.writeText(text);
      setIsCopy(true);
      return true;
    } catch (error) {
      console.error(error);
      setIsCopy(false);
       return false;
    }
  };
  return [isCopy, setIsCopy, copyClipboard];
}

그런데 이 방법은 한 뷰에서 연속적으로 복사하려면 isCopy를 false로 초기화해주는 과정이 필요했다.
또한, isCopy에 따라 액션을 실행하기 위해 useEffect를 활용했는데 마운트 될 때 쓸데없이 해당 액션이 실행되는 버그를 잡기가 까다로웠다.ㅠ

const [isCopy, setIsCopy, copyClipboard] = useCopyClipboard();

useEffect(() => {
  if (isCopy) {
    fireToast({ content: '링크가 클립보드에 저장되었습니다.', bottom: 190 });
    setIsCopy(false);
  } else {
    fireToast({ content: '다시 시도해주세요.', bottom: 190 });
  }
}, [isCopy]);

좋은 웹페이지 즐겨찾기