본문 바로가기

React

React 앱에서 Web Share API 사용하여 콘텐츠 공유하기

Web Share API는 사용자의 네이티브 공유 시트(공유 패널)를 호출하여 링크, 텍스트, 파일을 간편하게 공유할 수 있게 해주는 브라우저 API입니다. 모바일 중심의 UX를 구현할 때 클릭 한 번으로 카카오톡/메시지/메일 등으로 공유할 수 있어 이탈을 줄이고 전환을 높이는 데 효과적입니다. 이 글에서는 React 앱에서 Web Share API를 안정적으로 사용하고, 미지원 환경을 위한 폴백까지 실무적으로 구현하는 방법을 정리합니다.

1. 지원 범위와 핵심 포인트

- navigator.share: 텍스트/제목/URL 공유를 지원합니다.
- navigator.canShare 및 files 옵션: 이미지/파일 공유(레벨 2). 일부 브라우저/플랫폼에서만 동작합니다.
- 사용자 제스처 필요: 클릭 등 명시적 사용자 이벤트 컨텍스트에서 호출해야 합니다.
- HTTPS 권장: 보안 문맥에서 사용을 권장합니다(PWA/프로덕션 기본).

2. 가용성 체크(Feature Detection)

실서비스에서는 가용성 체크 후 폴백을 적용합니다. 아래 유틸은 클라이언트 환경에서 Web Share 가능 여부를 빠르게 판단합니다.

export function isWebShareSupported() {
  return typeof navigator !== 'undefined' && typeof navigator.share === 'function';
}

export function isFileShareSupported(files) {
  if (typeof navigator === 'undefined' || typeof navigator.canShare !== 'function') return false;
  try {
    return navigator.canShare({ files }) === true;
  } catch {
    return false;
  }
}

3. 기본 ShareButton 컴포넌트(링크/텍스트)

가장 흔한 패턴은 제목, 설명, URL을 넘겨 공유하는 방식입니다. SSR 환경(Next.js 등)에서 window 접근을 주의합니다.

import React, { useCallback, useMemo } from 'react';

export function ShareButton({ title, text, url, children }) {
  const shareData = useMemo(() => {
    if (typeof window === 'undefined') return { title, text, url };
    return {
      title: title || document.title,
      text: text || '',
      url: url || window.location.href,
    };
  }, [title, text, url]);

  const onShare = useCallback(async () => {
    if (!navigator?.share) {
      // 폴백으로 클립보드 복사 또는 소셜 링크 노출
      alert('이 브라우저에서는 기본 공유가 지원되지 않습니다. 링크를 클립보드에 복사합니다.');
      try {
        await navigator?.clipboard?.writeText?.(shareData.url || '');
      } catch {
        // 마지막 폴백: 선택 영역 복사 안내
      }
      return;
    }
    try {
      await navigator.share(shareData);
    } catch (err) {
      // 사용자가 취소했거나 오류가 발생
      // console.debug('Share canceled or failed', err);
    }
  }, [shareData]);

  return (
    <button type="button" onClick={onShare}>
      {children || '공유하기'}
    </button>
  );
}

4. 이미지/파일 공유(가능한 환경에서만)

파일 공유는 canShare와 files를 같이 사용해야 안전합니다. 예시는 원격 이미지를 Blob으로 받아 File로 변환한 뒤 공유합니다.

async function fetchImageAsFile(src, fileName = 'image.jpg', mime = 'image/jpeg') {
  const res = await fetch(src, { mode: 'cors' });
  const blob = await res.blob();
  return new File([blob], fileName, { type: mime });
}

export async function shareWithImage({ title, text, url, imageUrl }) {
  if (!navigator?.share) throw new Error('Web Share not supported');

  let files = [];
  try {
    if (imageUrl) {
      const file = await fetchImageAsFile(imageUrl);
      files = [file];
    }
  } catch {
    // 이미지 가져오기 실패 시 파일 없이 진행
  }

  const canShareFiles = files.length > 0 ? (navigator.canShare?.({ files }) || false) : false;
  const data = canShareFiles ? { title, text, url, files } : { title, text, url };

  return navigator.share(data);
}

주의: iOS Safari/안드로이드 Chrome 등에서 동작이 다를 수 있으며, 데스크톱은 제한적입니다. 파일 공유가 실패하면 텍스트/링크 공유로 자동 폴백하는 전략이 안전합니다.

5. Next.js 등 SSR 환경에서의 안전 처리

SSR 렌더링 중에는 navigator가 없으므로 조건부 로직이 필요합니다. 또는 동적 임포트를 통해 클라이언트 전용 컴포넌트로 분리합니다.

// 1) 클라이언트 체크 플래그
import { useEffect, useState } from 'react';

export function useIsClient() {
  const [isClient, setIsClient] = useState(false);
  useEffect(() => setIsClient(true), []);
  return isClient;
}

// 2) Next.js dynamic import (pages/App 라우팅 기준)
// import dynamic from 'next/dynamic';
// const ShareButton = dynamic(() => import('./ShareButton').then(m => m.ShareButton), { ssr: false });

6. 폴백: 클립보드 복사 + 소셜 링크

Web Share 미지원 환경에서는 링크 복사와 주요 플랫폼 공유 URL을 제공해 전환을 유지합니다.

export async function copyToClipboard(text) {
  if (navigator?.clipboard?.writeText) {
    await navigator.clipboard.writeText(text);
    return true;
  }
  // 레거시 폴백
  const input = document.createElement('input');
  input.value = text;
  document.body.appendChild(input);
  input.select();
  const ok = document.execCommand('copy');
  document.body.removeChild(input);
  return ok;
}

export function buildShareUrls({ url, text = '', via = '', utm = 'utm_source=share&utm_medium=referral' }) {
  const shareUrl = url.includes('utm_') ? url : `${url}${url.includes('?') ? '&' : '?'}${utm}`;
  const u = encodeURIComponent(shareUrl);
  const t = encodeURIComponent(text);
  const v = encodeURIComponent(via);
  return {
    twitter: `https://twitter.com/intent/tweet?url=${u}&text=${t}&via=${v}`,
    facebook: `https://www.facebook.com/sharer/sharer.php?u=${u}`,
    linkedin: `https://www.linkedin.com/sharing/share-offsite/?url=${u}`,
    whatsapp: `https://wa.me/?text=${t}%20${u}`,
    email: `mailto:?subject=${t}&body=${t}%20${u}`,
  };
}

팁: 공유 링크에 utm 파라미터를 붙여 유입 채널을 분석하세요. 지역 타깃팅 캠페인이라면 utm_campaign에 지역 코드(예: KR-SEOUL)를 포함하면 GEO 분석에 유용합니다.

7. 접근성/UX 체크리스트

- 버튼은 button 요소로 role/키보드 포커스가 보장되도록 합니다.
- 아이콘+라벨 텍스트를 함께 제공해 스크린리더 접근성을 확보합니다.
- 공유 완료/복사 완료 토스트를 노출해 피드백을 제공합니다.
- 공유 호출은 클릭 이벤트 안에서 즉시 실행합니다(비동기 Promise 체인은 OK).

8. 에러/취소 처리와 분석

사용자가 공유를 취소하는 것은 정상 흐름입니다. 오류와 구분하세요. 분석 이벤트는 성공 시에만 전송합니다.

async function safeShare(shareData, onSuccess) {
  if (!navigator?.share) throw new Error('not-supported');
  try {
    await navigator.share(shareData);
    onSuccess?.(); // e.g., analytics.track('share_success', ...)
  } catch (e) {
    if (e?.name === 'AbortError') {
      // 사용자가 취소
      return;
    }
    // 실제 오류 로깅
    // console.error(e);
  }
}

9. PWA와 Share Target

콘텐츠를 "보내기"만 하는 Web Share와 달리, 다른 앱에서 받은 공유를 우리 앱이 "받으려면" Web Share Target(매니페스트 + 서비스워커)이 필요합니다. 이는 별도 구성이며, 설치형 PWA에서 주로 사용합니다.

10. 실전 조합 예시

웹 공유 지원 시 네이티브 공유, 미지원 시 소셜 링크/복사 토글을 함께 제공하는 예시입니다.

import React, { useMemo } from 'react';
import { isWebShareSupported, copyToClipboard, buildShareUrls } from './share-utils';

export function SmartShare({ title, text, url }) {
  const supported = isWebShareSupported();
  const shareUrls = useMemo(() => buildShareUrls({ url, text, via: 'myreactapp' }), [url, text]);

  const onCopy = async () => {
    await copyToClipboard(url);
    alert('링크가 복사되었습니다');
  };

  const onShare = async () => {
    try {
      await navigator.share({ title, text, url });
    } catch {}
  };

  return (
    <div>
      {supported ? (
        <button onClick={onShare}>공유하기</button>
      ) : (
        <div>
          <a href={shareUrls.twitter} target="_blank" rel="noreferrer">X(트위터)</a>
          {' | '}
          <a href={shareUrls.facebook} target="_blank" rel="noreferrer">페이스북</a>
          {' | '}
          <a href={shareUrls.linkedin} target="_blank" rel="noreferrer">링크드인</a>
          {' | '}
          <button onClick={onCopy}>링크 복사</button>
        </div>
      )}
    </div>
  );
}

정리

React에서 Web Share API를 적용할 때는 1) 기능 감지로 안전하게 호출하고, 2) 사용자 제스처 안에서 실행하며, 3) 파일 공유는 canShare로 분기, 4) 미지원 환경에 클립보드/소셜 링크 폴백을 제공하는 것이 핵심입니다. 공유 링크에는 UTM을 포함해 AEO/SEO 성과를 추적하고, 지역 캠페인은 GEO 태깅으로 분석을 강화하세요. 위 예제들을 조합하면 모바일 중심의 매끄러운 공유 UX를 빠르게 구축할 수 있습니다.