2024-08-20 이벤트 전파(event bubbling)
문제 상황
사용자의 프로필 카드를 만들었다.
그리고 그 프로필 카드 우측 상단에 하트를 만들었고, 하트를 클릭하면 팔로우가 되도록 하였다.
follow API route가 다른 곳에서도 재사용되고 있는데 optimistic하게 보여줄 수 없는 페이지도 있어서
optimistic update는 아니고,
기대하는 상황은 하트를 누르면 사용자의 프로필로 넘어가는 것이 아니라 (프로필은 카드 자체를 누르면 넘어가게 링크되어 있다.)
팔로우 기능만 작동하기를 바란다.
그런데 팔로우 기능만 작동하는 것이 아니라 팔로우 버튼이 눌림과 동시에 카드 자체도 눌려진다.
버튼 위에 버튼이 있는 형상이다.
즉 자식 요소의 클릭 이벤트가 부모 요소의 클릭 이벤트까지 전파가 되었다는 것을 의미하고,
이를 이벤트 버블링이라 한다. 마치 거품이 아래에서부터 보글보글 위로 솟아나는 모습을 생각하면 이해가 쉬울 것이다.
아래 첨부한 영상은 개발 서버라 끔찍하게 느리다. 잘못 만든 것 아니니 참고바람.
문제의 코드
보기 편하도록 일부러 컴포넌트 분리를 하지 않은 상태임을 감안하고 보시기 바람.
import BuddyTemperature from '@/components/atoms/profile/BuddyTemperature';
import Image from 'next/image';
import Link from 'next/link';
import { Buddy } from '@/types/Auth.types';
import MascotImage from '@/components/atoms/common/MascotImage';
import { getAgeFromBirthDate } from '@/utils/common/getAgeFromBirthDate';
import React, { useState, useEffect } from 'react';
import { useAuth } from '@/hooks';
function HomePageRecommendBuddiesList({
buddies,
className,
}: {
buddies: Buddy[];
className?: string;
}) {
const { buddy: currentBuddy } = useAuth();
return (
<>
{buddies
? buddies.map((buddy: Buddy, index: number) => (
<Link
key={index}
href={`/profile/${buddy.buddy_id}`}
passHref
>
<div
className={`relative min-w-[200px] h-[75px] mx-1 rounded border border-gray-200 cursor-pointer flex items-center ${className}`}
>
<div className="flex items-center justify-center w-full h-full relative">
<div className="flex-shrink-0 w-[75px] h-[75px] flex items-center justify-center">
{buddy.buddy_profile_pic ? (
<Image
src={buddy.buddy_profile_pic}
alt="profile"
width={60}
height={60}
className="rounded-lg w-[60px] h-[60px]"
/>
) : (
<MascotImage intent="happy" />
)}
</div>
<div className="mx-1 flex flex-col w-full relative">
<span className="text-xs font-bold text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis">
{buddy.buddy_preferred_buddy1 &&
buddy.buddy_preferred_buddy2
? `#${buddy.buddy_preferred_buddy1} #${buddy.buddy_preferred_buddy2}`
: '#태그없음'}
</span>
<div className="text-m font-bold whitespace-nowrap overflow-hidden text-ellipsis w-full">
<span className="block truncate">
{buddy.buddy_nickname}
{typeof buddy.buddy_birth ===
'string'
? ` / ${getAgeFromBirthDate(
buddy.buddy_birth,
)} 세`
: null}
</span>
</div>
<div className="w-full flex justify-between items-center">
<BuddyTemperature
isLabel={false}
isTempText={false}
temperature={
buddy.buddy_temperature
}
/>
</div>
</div>
</div>
<FollowButton
followingId={buddy.buddy_id}
followerId={currentBuddy?.buddy_id || ''}
/>
</div>
</Link>
))
: null}
</>
);
}
function FollowButton({
followingId,
followerId,
}: {
followingId: string;
followerId: string;
}) {
const [isFollowing, setIsFollowing] = useState(false);
useEffect(() => {
// 기존 팔로우 여부 검사
const checkFollowStatus: () => Promise<void> = async () => {
const res = await fetch(
`/api/buddyProfile/follow?followingId=${followingId}&followerId=${followerId}`,
);
const data = await res.json();
setIsFollowing(data.originFollow.length > 0);
};
checkFollowStatus();
}, [followingId, followerId]);
const handleFollowToggle = async () => {
if (isFollowing) {
await fetch(
`/api/buddyProfile/follow?followingId=${followingId}&followerId=${followerId}`,
{ method: 'DELETE' },
);
} else {
await fetch(`/api/buddyProfile/follow`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
followingId,
followerId,
}),
});
}
setIsFollowing(!isFollowing);
};
return (
<button
className="absolute top-0 right-0 text-xl"
onClick={handleFollowToggle}
>
<span className={isFollowing ? 'text-main-color' : ''}>
{isFollowing ? '♥' : '♡'}
</span>
</button>
);
}
export default HomePageRecommendBuddiesList;
문제의 원인
<Link> 컴포넌트 안에 클릭 이벤트가 발생할 수 있는 <button> 태그를 넣었기 때문에 이런 현상이 발생했다.
<button> 태그 뿐만 아니라 <a> 태그 역시 이런 현상이 발생할 것이다.
그렇다면 해결 방법은 보이는 것 같다.
<Link> 컴포넌트, 즉 부모 요소를 클릭 이벤트가 발생하는 컴포넌트나 태그가 아니라
그 자체로는 클릭 이벤트를 지원하지 않는 일반 <div> 태그로 변경하고,
클릭을 했을 때 별도의 이벤트가 발생하도록 onClick 이벤트 핸들러로 함수를 연결하면 된다.
Next.js에서는 <Link> 컴포넌트와 사실상 같은 방법이 next/navigation에서 제공하는 useRouter 훅의 router.push('/') 기능이다.
이것을 이용해서 이동만을 지원하는 함수를 만들고 연결하면 해결이 된다.
해결
import BuddyTemperature from '@/components/atoms/profile/BuddyTemperature';
import Image from 'next/image';
import { Buddy } from '@/types/Auth.types';
import MascotImage from '@/components/atoms/common/MascotImage';
import { getAgeFromBirthDate } from '@/utils/common/getAgeFromBirthDate';
import React, { useState, useEffect } from 'react';
import { useAuth } from '@/hooks';
import { useRouter } from 'next/navigation';
function HomePageRecommendBuddiesList({
buddies,
className,
}: {
buddies: Buddy[];
className?: string;
}) {
const { buddy: currentBuddy } = useAuth();
const router = useRouter();
const handleCardClick = (buddyId: string) => {
router.push(`/profile/${buddyId}`);
};
return (
<>
{buddies
? buddies.map((buddy: Buddy, index: number) => (
<div
key={index}
className={`relative min-w-[200px] h-[75px] mx-1 rounded border border-gray-200 cursor-pointer flex items-center ${className}`}
onClick={() => handleCardClick(buddy.buddy_id)}
>
<div className="flex items-center justify-center w-full h-full relative">
<div className="flex-shrink-0 w-[75px] h-[75px] flex items-center justify-center">
{buddy.buddy_profile_pic ? (
<Image
src={buddy.buddy_profile_pic}
alt="profile"
width={60}
height={60}
className="rounded-lg w-[60px] h-[60px]"
/>
) : (
<MascotImage intent="happy" />
)}
</div>
<div className="mx-1 flex flex-col w-full relative">
<span className="text-xs font-bold text-gray-500 whitespace-nowrap overflow-hidden text-ellipsis">
{buddy.buddy_preferred_buddy1 &&
buddy.buddy_preferred_buddy2
? `#${buddy.buddy_preferred_buddy1} #${buddy.buddy_preferred_buddy2}`
: '#태그없음'}
</span>
<div className="text-m font-bold whitespace-nowrap overflow-hidden text-ellipsis w-full">
<span className="block truncate">
{buddy.buddy_nickname}
{typeof buddy.buddy_birth ===
'string'
? ` / ${getAgeFromBirthDate(
buddy.buddy_birth,
)} 세`
: null}
</span>
</div>
<div className="w-full flex justify-between items-center">
<BuddyTemperature
isLabel={false}
isTempText={false}
temperature={
buddy.buddy_temperature
}
/>
</div>
</div>
</div>
<FollowButton
followingId={buddy.buddy_id}
followerId={currentBuddy?.buddy_id || ''}
onClick={(e) => e.stopPropagation()} // 추가: 하트 버튼에서 이벤트 전파 중단
/>
</div>
))
: null}
</>
);
}
function FollowButton({
followingId,
followerId,
onClick,
}: {
followingId: string;
followerId: string;
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void; // 추가: onClick 이벤트 전달
}) {
const [isFollowing, setIsFollowing] = useState(false);
useEffect(() => {
// 기존 팔로우 여부 검사
const checkFollowStatus: () => Promise<void> = async () => {
const res = await fetch(
`/api/buddyProfile/follow?followingId=${followingId}&followerId=${followerId}`,
);
const data = await res.json();
setIsFollowing(data.originFollow.length > 0);
};
checkFollowStatus();
}, [followingId, followerId]);
const handleFollowToggle = async (
e: React.MouseEvent<HTMLButtonElement>,
) => {
onClick(e); // 추가: onClick 이벤트 실행
if (isFollowing) {
await fetch(
`/api/buddyProfile/follow?followingId=${followingId}&followerId=${followerId}`,
{ method: 'DELETE' },
);
} else {
await fetch(`/api/buddyProfile/follow`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
followingId,
followerId,
}),
});
}
setIsFollowing(!isFollowing);
};
return (
<button
className="absolute top-0 right-0 text-xl"
onClick={handleFollowToggle}
>
<span className={isFollowing ? 'text-main-color' : ''}>
{isFollowing ? '♥' : '♡'}
</span>
</button>
);
}
export default HomePageRecommendBuddiesList;
결과는? 아주 마음에 든다. 흡족하다.
'Programing > TIL' 카테고리의 다른 글
2024-08-22 컴포넌트 재사용에 따른 조건부 로직 구현 방법 (0) | 2024.08.22 |
---|---|
2024-08-21 광클을 방지하기 위한 노력 (0) | 2024.08.21 |
2024-08-19 [Next.js, Supabase] 여러 테이블을 참조하는 DELETE 메서드 작성하기 (0) | 2024.08.19 |
2024-08-16 미인증 사용자에게는 블러 처리된 오버레이 모달 렌더링하기 (0) | 2024.08.16 |
2024-08-14 브라우저의 렌더링 과정 이해하기 (0) | 2024.08.14 |
댓글