본문 바로가기
React

[React] navigator.share + clipboard로 공유 버튼 간단 구현하기

by teamnova 2025. 8. 25.
728x90

안녕하세요 오늘은 React 프로젝트 카드나 블로그 게시글 리스트에 간단한 공유 버튼(공유 or 복사) 을 추가하는 방법을 소개해보려고 합니다.

 

공유 버튼 사용 이유 

블로그 글이나 포트폴리오 프로젝트는 결국 누군가에게 보여주려고 작성하는 글입니다. 링크를 쉽게 공유할 수 있는 버튼 하나만 달아줘도 독자 입장에서는 주소창 열어서 복사/붙여넣기 하는 수고를 덜 수 있습니다. 작은 디테일이지만 체감 UX가 확 달라집니다 

 

요즘은 모바일에서 글을 보는 경우가 많습니다. 주소창 열어서 링크 복사하기는 은근 번거로운데, 공유 버튼을 누르면 바로 카톡/메일/sns 로 바로 보낼 수 있습니다. 네이티브 공유 시트가 뜨기 때문에 “이거 공유해야겠다” 싶은 순간 바로 행동으로 이어집니다. 

 

  • 모바일: 네이티브 공유 시트(navigator.share)가 열려서 카톡/메일 등으로 바로 전송 가능
  • 데스크탑: 지원 안 되는 경우 자동으로 링크를 클립보드 복사

 

사용한 브라우저 API & 라이브러리

  1. navigator.share
    • 모바일 브라우저(특히 iOS Safari, Android Chrome)에서 네이티브 공유 시트를 띄워줍니다.
    • 단, 모든 브라우저에서 지원하는 건 아니기 때문에 지원하지 않는 경우를 대비한 대체 로직이 필요합니다. 
  2. navigator.clipboard
    • HTTPS 환경에서 안전하게 클립보드 복사를 지원합니다
  3. react-hot-toast
    • 복사 완료 시 피드백을 깔끔하게 보여주기 위해 사용했습니다.
    • alert() 대신 토스트 알림을 쓰면 UI가 훨씬 자연스럽습니다.

 

1. share button 컴포넌트 

import { toast } from "react-hot-toast"

export default function ShareButton({ url, title }) {
  const canWebShare = typeof navigator !== "undefined" && !!navigator.share
  const shareUrl = url || window.location.href

  const copyFallback = async () => {
    try {
      await navigator.clipboard.writeText(shareUrl)
      toast.success("링크를 복사했어요!")
    } catch {
      // 구식 브라우저 폴백
      const ta = document.createElement("textarea")
      ta.value = shareUrl
      document.body.appendChild(ta)
      ta.select()
      document.execCommand("copy")
      ta.remove()
      toast.success("링크를 복사했어요!")
    }
  }

  const onClick = async () => {
    if (canWebShare) {
      try {
        await navigator.share({ url: shareUrl, title })
      } catch {
        // 사용자가 공유 취소 시 무시
      }
    } else {
      await copyFallback()
    }
  }

  return (
    <button
      onClick={onClick}
      className="ml-3 rounded border px-2 py-1 text-xs hover:bg-gray-50 
                 dark:hover:bg-zinc-800 inline-flex items-center gap-1"
    >
      🔗 공유
    </button>
  )
}

 

 

이제 위 버튼을 공유하고자 하는 게시글 or 프로젝트 카드 리스트에 적용해줍니다. 

이 게시글에서는 기존 프로젝트 목록을 보여주는 ui 에 공유 버튼을 추가하고자합니다. 

 

2. 기존 프로젝트 목록 

## 프로젝트 목록 


function ProjectGallery() {
  const [activeFilter, setActiveFilter] = useState('all')
  
  const projects = [
    {
      id: 1,
      title: "E-Commerce Platform",
      category: "web",
      image: "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=400",
      description: "Full-stack e-commerce platform with React and Node.js",
      tech: ["React", "Node.js", "MongoDB", "Stripe"],
      link: "#"
    },
    {
      id: 2,
      title: "Task Management App",
      category: "mobile",
      image: "https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=400",
      description: "Cross-platform mobile app for task management",
      tech: ["React Native", "Firebase", "Redux"],
      link: "#"
    },
    {
      id: 3,
      title: "Portfolio Website",
      category: "web",
      image: "https://images.unsplash.com/photo-1460925895917-afdab827c52f?w=400",
      description: "Modern portfolio website with animations",
      tech: ["React", "Framer Motion", "Tailwind CSS"],
      link: "#"
    },
    {
      id: 4,
      title: "Weather Dashboard",
      category: "web",
      image: "https://images.unsplash.com/photo-1592210454359-9043f067919b?w=400",
      description: "Real-time weather dashboard with charts",
      tech: ["Vue.js", "Chart.js", "OpenWeather API"],
      link: "#"
    },
    {
      id: 5,
      title: "Fitness Tracker",
      category: "mobile",
      image: "https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?w=400",
      description: "Mobile app for tracking workouts and nutrition",
      tech: ["Flutter", "SQLite", "Google Fit API"],
      link: "#"
    },
    {
      id: 6,
      title: "Social Media Dashboard",
      category: "web",
      image: "https://images.unsplash.com/photo-1611162617213-7d7a39e9b1d7?w=400",
      description: "Analytics dashboard for social media management",
      tech: ["Angular", "D3.js", "Express.js"],
      link: "#"
    }
  ]
  
  
  
  ## 프로젝트 ui 
   return (
    <section className="py-20 bg-gray-50">
      <div className="max-w-7xl mx-auto px-6">
        {/* 헤더 */}
        <div className="text-center mb-12">
          <h2 className="text-4xl font-bold text-gray-900 mb-4">
            My Projects
          </h2>
          <p className="text-xl text-gray-600 max-w-3xl mx-auto">
            Explore my latest work across web development, mobile apps, and creative solutions.
          </p>
        </div>

        {/* 필터 버튼 */}
        <div className="flex justify-center mb-8 space-x-4">
          {['all', 'web', 'mobile'].map(filter => (
            <button
              key={filter}
              onClick={() => setActiveFilter(filter)}
              className={`px-6 py-2 rounded-full font-medium transition-colors ${
                activeFilter === filter
                  ? 'bg-blue-600 text-white'
                  : 'bg-white text-gray-600 hover:bg-gray-100'
              }`}
            >
              {filter === 'all' ? 'All Projects' : 
               filter === 'web' ? 'Web Apps' : 'Mobile Apps'}
            </button>
          ))}
        </div>

        {/* 프로젝트 그리드 */}
        <div className="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
          {filteredProjects.map(project => (
            <div
              key={project.id}
              className="bg-white rounded-xl shadow-lg overflow-hidden hover:shadow-xl transition-shadow duration-300"
            >
              {/* 이미지 */}
              <div className="relative h-48 overflow-hidden">
                <img
                  src={project.image}
                  alt={project.title}
                  className="w-full h-full object-cover hover:scale-110 transition-transform duration-300"
                />
                <div className="absolute top-4 right-4">
                  <span className={`px-3 py-1 rounded-full text-xs font-medium ${
                    project.category === 'web' 
                      ? 'bg-blue-100 text-blue-800' 
                      : 'bg-green-100 text-green-800'
                  }`}>
                    {project.category === 'web' ? 'Web' : 'Mobile'}
                  </span>
                </div>
              </div>

              {/* 내용 */}
              <div className="p-6">
                <h3 className="text-xl font-bold text-gray-900 mb-2">
                  {project.title}
                </h3>
                <p className="text-gray-600 mb-4">
                  {project.description}
                </p>
                
                {/* 기술 스택 */}
                <div className="flex flex-wrap gap-2 mb-4">
                  {project.tech.map(tech => (
                    <span
                      key={tech}
                      className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded"
                    >
                      {tech}
                    </span>
                  ))}
                </div>

                {/* 링크 버튼 */}
                <a
                  href={project.link}
                  className="inline-flex items-center text-blue-600 hover:text-blue-700 font-medium"
                >
                  View Project
                  <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                    <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
                  </svg>
                </a>

                   <ShareButton slug={project.id} title={project.title} />
              </div>
            </div>
          ))}
        </div>
      </div>
    </section>
  )
}

export default ProjectGallery

 

 

 

3. 링크 공유 버튼 추가하기 

return 내부에 있는 ui 가장 하단 부분에 

shrare button 을 추가해줍니다. 

<ShareButton slug={project.id} title={project.title} />

 

 

프로젝트마다 공유버튼이 생성된 것을 확인할 수 있습니다.