프론트엔드 개발자
류준열

사파리에서 클립보드 복사 이슈

navigator.clipboard.writeText는 웹 api의 일부로 시스템 클립보드에 텍스트를 쓰는 기능을 제공한다. 비동기적으로 동작하면서 Promise를 반환한다.

다음과 같이 url 복사기능을 구현했는데, 사파리에서만 catch가 실행되었다.

const ReferralPage = () => {

  const copyInvitationUrl = async () => {
    const referralCode = await getReferralCode(); // api call
    const link = `${window.location.origin}/referred?referral-code=${referralCode}`;
    await navigator.clipboard
      .writeText(link)
      .then(() => makeToast({ isSuccess: true, content: '링크를 복사했어요.' }))
      .catch(() => makeToast({ isSuccess: false, content: `으악 실패했어요! ${navigator.userAgent}` }));
  };
 
 return (... 
        <InvitationUrlCopyButton onClick={CopyInvitationUrl}>
            초대링크 복사하기 <img src={HeartImg} alt="하트이미지" />
         </InvitationUrlCopyButton>
)}

사파리에서 복사기능 구현시 주의할 점

알아보니 사파리에서는 보안상의 이유로 유저 액션을 통해서만 navigator.clipboard.writeText가 동작하는데, 내가 작성한 코드에는 복사 전에 api call이 있었고 이를 보안상 이슈로 판단해 사파리 브라우저에서 자체적으로 catch를 실행시킨 것이다. (유저 클릭 액션 -> api call -> 복사)

  const CopyInvitationUrl = async () => {
    const referralCode = await getReferralCode(); // api call

    const link = `${window.location.origin}/referred?referralCode=${referralCode}`;
    await navigator.clipboard
      .writeText(link)
      .then(() => makeToast({ isSuccess: true, content: '링크를 복사했어요.' }))
      .catch(() => makeToast({ isSuccess: false, content: `으악 실패했어요! ${navigator.userAgent}` }));
  };

그래서 일단 onClick에는 복사기능만 남겨두었다. (유저 클릭 액션 -> 복사)

const CopyInvitationUrl = async () => {
    // api call 삭제 
    // const referralCode = await getReferralCode(); 
    
    const link = `${window.location.origin}/referred?referralCode=${referralCode}`;
    await navigator.clipboard
      .writeText(link)
      .then(() => makeToast({ isSuccess: true, content: '링크를 복사했어요.' }))
      .catch(() => makeToast({ isSuccess: false, content: `으악 실패했어요! ${navigator.userAgent}` }));
  };

그럼 api call 어디서함?

처음에는 api call을 useLayoutEffect안에 넣었다. 해당 api로 ui 가 변경되지 않기 때문에 paint 전에 불러와도 되기 때문이다.

하지만 복사를 할지 안할지도 모르는데 무조건 통신하는 방식이 맘에 들지 않아서 계속 고민하다가 onMouseOver가 떠올랐다.

버튼 주변에 더 넓은 box를 만들고 해당 box에 마우스를 올리면 api call을 하도록 변경하였다.

        <Wrapper onMouseOver = {getReferralCode}>
          <InvitationUrlCopyButton onClick={CopyInvitationUrl} onMouseOver={() => getReferral(profile)}>
            초대링크 복사하기 <img src={HeartImg} alt="" />
          </InvitationUrlCopyButton>
        </Wrapper>

정리

사파리에서는 보안상의 이유로 유저 액션을 통해서만 navigator.clipboard.writeText가 동작함.
onClick이벤트에 api call 받고 복사시키면 에러반환함.
그렇기 때문에 api call을 onClick에 넣으면 안됨.
나는 주변 박스 크게 만들고 그 박스에 마우스 올리면 api call 하도록 했음