본문 바로가기

카테고리 없음

Fiesta Tour_9일차

refetch는 react-query 라이브러리에서 데이터를 다시 가져오는 기능을 제공하는 함수입니다.

useQuery 훅을 사용할 때, 데이터를 불러오는 함수(queryFn)가 실행되어 데이터를 가져오고, 이후 refetch 함수를 호출하여 동일한 데이터를 다시 요청할 수 있습니다.

주요 기능

  1. 데이터 갱신: refetch를 호출하면 queryFn이 다시 실행되어 최신 데이터를 가져옵니다.
  2. 강제 갱신: 캐시된 데이터를 사용하지 않고 강제로 데이터를 새로 불러올 때 유용합니다.
  3. 조건부 데이터 요청: 특정 이벤트(예: 버튼 클릭, 페이지 전환 등) 발생 시 데이터를 다시 불러올 수 있습니다.
  const { data, isPending, error, refetch } = useQuery<Tables<'posts'>[]>({
    queryKey: ['likedPosts', userId],
    queryFn: getLikedPostsData,
    enabled: !!userId
  });

  useEffect(() => {
    refetch();
  }, [userId, refetch]);

 

supabase : select 응용

const { data, error } = await supabase
  .from('likes')
  .select('post_id, posts (id, title, content, image, created_at)')
  .eq('user_id', userId);
  1. supabase.from('likes'):
    • likes 테이블을 선택합니다. 이 테이블은 사용자가 좋아요를 누른 게시글의 정보를 포함하고 있습니다.
  2. .select('post_id, posts (id, title, content, image, created_at)'):
    • select 메서드는 쿼리에서 반환할 컬럼을 지정합니다.
    • 'post_id': likes 테이블에서 post_id 컬럼을 선택합니다. 이 컬럼은 좋아요한 게시글의 ID를 나타냅니다.
    • 'posts (id, title, content, image, created_at)': 관계형 데이터를 조인하여 posts 테이블에서 선택할 컬럼을 지정합니다. posts 테이블은 게시글의 상세 정보를 포함합니다.
      • id: 게시글의 ID.
      • title: 게시글의 제목.
      • content: 게시글의 내용.
      • image: 게시글의 이미지 URL.
      • created_at: 게시글의 생성 날짜.
    이 구문은 likes 테이블과 posts 테이블을 조인하여, posts 테이블의 특정 컬럼을 선택합니다.
  3. .eq('user_id', userId):
    • eq 메서드는 조건을 추가합니다. 여기서는 user_id 컬럼이 userId와 일치하는 행을 선택합니다.
    • userId는 특정 사용자의 ID입니다. 따라서, 이 조건은 특정 사용자가 좋아요를 누른 게시글만 선택합니다.

전체 설명

이 쿼리는 likes 테이블에서 특정 사용자가 좋아요를 누른 게시글의 post_id를 가져오고, 해당 post_id와 조인된 posts 테이블에서 게시글의 상세 정보를 가져옵니다. 이를 통해 특정 사용자가 좋아요를 누른 게시글 목록을 가져올 수 있습니다.

 

 

HTML 이스케이프

 

HTML 태그가 그대로 화면에 나타나는 현상은 "HTML 이스케이프" 또는 "HTML 인코딩"이라고 합니다. 이는 브라우저가 HTML 태그를 일반 텍스트로 처리하여 태그를 해석하지 않고 그대로 표시하는 것입니다.

dangerouslySetInnerHTML을 사용하면 HTML을 직접 삽입할 수 있어 유연한 콘텐츠 렌더링이 가능하지만, 보안 취약점이 발생할 수 있습니다. 주된 문제점은 XSS(Cross-Site Scripting) 공격입니다.

문제점

  1. XSS 공격:
    • 사용자 입력을 신뢰하지 않고 그대로 렌더링하면 악의적인 스크립트가 실행될 수 있습니다. 이는 사용자의 쿠키, 세션 토큰 등을 탈취하거나 악성 코드 실행에 이용될 수 있습니다.
  2. 데이터 신뢰성:
    • 외부에서 가져온 데이터를 신뢰할 수 없기 때문에 데이터가 예상치 못한 HTML로 변형될 수 있습니다.
  3. 유지보수 어려움:
    • 프로젝트가 커질수록 관리하기 어렵고, 보안 문제가 발생했을 때 추적하기 어렵습니다.

대안

  1. 데이터를 클린(청소)하기:
    • 사용자가 입력한 데이터를 삽입하기 전에 이를 클린(clean)하여 안전한 HTML만 남기고 나머지를 제거합니다.
    • 라이브러리 사용: DOMPurify, sanitize-html 등.
  2. React 컴포넌트 사용:
    • 가능하면 dangerouslySetInnerHTML를 피하고, React 컴포넌트를 사용하여 안전하게 데이터를 렌더링합니다.

1. DOMPurify와 타입 정의 파일을 설치합니다.

yarn add dompurify
yarn add -D @types/dompurify

2. components/SafeComponent.tsx 파일 생성

import DOMPurify from 'dompurify';
import React from 'react';

interface SafeComponentProps {
  title: string;
}

const SafeComponent: React.FC<SafeComponentProps> = ({ title }) => {
  const cleanHTML = DOMPurify.sanitize(title);

  return <h3 className="font-bold" dangerouslySetInnerHTML={{ __html: cleanHTML }} />;
};

export default SafeComponent;

3. pages/index.tsx

import React from 'react';
import SafeComponent from '../components/SafeComponent';

const Home: React.FC = () => {
  const title = "<script>alert('xss')</script><strong>Safe Title</strong>";

  return (
    <div>
      <SafeComponent title={title} />
    </div>
  );
};

export default Home;

설명

  1. components/SafeComponent.tsx:
    • DOMPurify를 사용하여 입력된 HTML 문자열을 클린(clean)합니다.
    • dangerouslySetInnerHTML를 사용하여 클린된 HTML을 안전하게 렌더링합니다.
  2. pages/index.tsx:
    • SafeComponent를 사용하여 HTML 문자열을 렌더링합니다.
    • 입력된 HTML 문자열이 DOMPurify를 통해 클린되기 때문에 XSS 공격을 방지할 수 있습니다.

 

 

throw new Error는 자바스크립트에서 오류를 발생시키는 방법 중 하나입니다. 이 메소드를 사용하면 프로그램의 흐름을 중단시키고, 지정된 오류 메시지를 포함한 새로운 Error 객체를 생성합니다.

throw new Error의 역할:

  • 오류 발생: throw new Error를 호출하면 코드 실행이 중단되고, 가장 가까운 try...catch 블록으로 제어가 이동합니다.
  • 에러 메시지: new Error 객체를 생성할 때 메시지를 전달할 수 있습니다. 이 메시지는 디버깅 및 로그 분석 시 유용하게 사용될 수 있습니다.
try {
  // 일부 코드 실행
  throw new Error('Something went wrong');
  // 이 줄은 실행되지 않음
} catch (error) {
  console.error(error.message);  // 'Something went wrong' 출력
}

장점:

  1. 클린 코드: console.log 대신 오류를 발생시키면 프로덕션 코드가 더 깔끔해집니다.
  2. 에러 핸들링: 특정 오류에 대해 예외를 발생시키고, 이를 catch 블록에서 처리할 수 있습니다.
  3. 디버깅 용이: 오류 메시지를 통해 어떤 오류가 발생했는지 쉽게 알 수 있습니다.

console.log 대신 throw new Error의 사용:

  • 개발 브랜치: console.log는 개발 중에는 유용하지만, 프로덕션 코드에는 남기지 않는 것이 좋습니다.
  • 프로덕션 코드: throw new Error는 오류를 명확히 처리하고, 애플리케이션이 예기치 않게 동작하지 않도록 도와줍니다. 예외 발생 시 알맞게 처리할 수 있는 구조를 마련하는 것이 좋습니다.

요약

  • console.log는 개발 중에 유용하지만, 프로덕션 코드에서는 과도한 로그 출력이나 민감한 정보 노출의 위험이 있어 사용을 최소화해야 합니다.
  • throw new Error는 예외 상황을 명확히 처리하고, 상위 코드에서 이를 적절히 핸들링할 수 있게 도와주는 방법으로, 프로덕션 코드에서도 사용하기 적합합니다.

 

마이페이지 좋아요 리스트 관리

페이지 내에서 좋아요 게시글 확인 가능 , 좋아요 취소 및 재좋아요 가능

LikeList.tsx

'use client';

import axios from 'axios';
import { useQuery } from '@tanstack/react-query';
import React, { useEffect } from 'react';
import { useParams } from 'next/navigation';
import Link from 'next/link';
import Image from 'next/image';
import useAuthStore from '@/zustand/bearsStore';
import { Tables } from '@/types/supabase';
import { API_MYPAGE_LIKES } from '@/utils/apiConstants';
import Like from './Like';

export default function LikeList() {
  const params = useParams();
  const userId = Array.isArray(params.id) ? params.id[0] : params.id;
  const user = useAuthStore((state) => state.user);

  const getLikedPostsData = async () => {
    try {
      const response = await axios.get(API_MYPAGE_LIKES(userId));
      const data: Tables<'posts'>[] = response.data.posts;
      return data;
    } catch (error) {
      if (axios.isAxiosError(error)) {
        throw new Error(`HTTP error! status: ${error.response?.status}`);
      } else {
        throw new Error('An unknown error occurred');
      }
    }
  };

  const { data, isPending, error, refetch } = useQuery<Tables<'posts'>[]>({
    queryKey: ['likedPosts', userId],
    queryFn: getLikedPostsData,
    enabled: !!userId
  });

  useEffect(() => {
    refetch();
  }, [userId, refetch]);

  if (isPending) return <div className="flex h-screen items-center justify-center">Loading...</div>;

  if (error) {
    return <div className="flex h-screen items-center justify-center">Error: {error.message}</div>;
  }

  if (!data || data.length === 0) {
    return <div className="flex h-screen items-center justify-center">No posts found</div>;
  }

  return (
    <div className="max-w-[360px]">
      {data.map((post) => (
        <div key={post.id} className="relative">
          <p>{new Date(post.created_at).toLocaleString()}</p>
          <Link href={`/detail/${post.id}`}>
            <div className="flex">
              <Image
                className="mb-[20px] mr-2"
                src={post.image ?? '/icons/upload.png'}
                alt={post.title ?? 'Default title'}
                width={76}
                height={76}
              />
              <p className="overflow-hidden text-ellipsis whitespace-nowrap text-[14px] font-bold">{post.title}</p>
            </div>
          </Link>
          <Like postId={post.id} userId={user.id} />
        </div>
      ))}
    </div>
  );
}

Like.tsx

'use client';

import axios from 'axios';
import React, { useEffect, useState } from 'react';
import { useQuery, useMutation } from '@tanstack/react-query';
import { FaRegHeart, FaHeart } from 'react-icons/fa';
import { API_MYPAGE_LIKES } from '@/utils/apiConstants';

type LikeProps = {
  postId: string;
  userId: string;
};

const Like: React.FC<LikeProps> = ({ postId, userId }) => {
  const [liked, setLiked] = useState(false);

  const fetchLikeStatus = async (): Promise<boolean> => {
    const response = await axios.get(API_MYPAGE_LIKES(userId));
    const likedPosts = response.data.posts.map((post: any) => post.id);
    return likedPosts.includes(postId);
  };

  const toggleLikeStatus = async (): Promise<void> => {
    await axios.put(API_MYPAGE_LIKES(userId), { postId });
  };

  const { data, isError, isLoading, refetch } = useQuery<boolean>({
    queryKey: ['likeStatus', postId, userId],
    queryFn: fetchLikeStatus,
    enabled: !!postId && !!userId
  });

  const likeMutation = useMutation<void, Error>({
    mutationFn: toggleLikeStatus,
    onSuccess: () => {
      refetch();
    }
  });

  const handleLike = () => {
    if (isLoading) return;
    likeMutation.mutate();
  };

  useEffect(() => {
    if (data !== undefined) {
      setLiked(data);
    }
  }, [data]);

  if (isLoading) return <div>Loading...</div>;
  if (isError) return <div>Error fetching like status</div>;

  return (
    <button onClick={handleLike} className="absolute right-0 top-0 p-2">
      {liked ? <FaHeart size={30} color="red" /> : <FaRegHeart size={30} />}
    </button>
  );
};

export default Like;

 

마이페이지 Custom Back Navigation

마이페이지 내에서 프로필 수정 , 지역 설정 등 내부 페이지 이동 시에도 마이페이지에서 뒤로 갈 시 접근했던 페이지로 이동 하도록 이동 설정 구현

  • router.back() 과 router.replace()를 이용하여 마이페이지 접근 시에는 push 혹은 Link를 통해 오므로 경로 기록이 남지만 마이페이지 내에서는 replace를 통해 경로 기록을 남기지 않고 마이페이지에서는 back으로 돌아가게 구현 -> 하지만 마이페이지내에서 수정 페이지 들 중에서 bakc버튼이 아닌 페이지 뒤로가기를 하면 접근 페이지로 돌아가는 문제발생 / 우선 앱 기준으로 개발중이므로 해결 보류

router.push

router.push는 사용자를 새로운 URL로 이동시킵니다. 브라우저의 히스토리에 새 항목이 추가되며, 사용자가 뒤로 가기 버튼을 눌러 이전 페이지로 돌아갈 수 있습니다.

import { useRouter } from 'next/navigation';

const MyComponent = () => {
  const router = useRouter();

  const goToPage = () => {
    router.push('/new-page');
  };

  return <button onClick={goToPage}>Go to new page</button>;
};

router.push의 인자

  • url: 이동할 경로 (문자열)
  • options: 추가 옵션 (선택 사항)
    • shallow: true로 설정하면, 페이지가 재렌더링되지 않고 URL만 변경됨
router.push('/new-page', { shallow: true });

router.replace

router.replace는 사용자를 새로운 URL로 이동시키지만, 브라우저 히스토리에 새 항목을 추가하지 않습니다. 즉, 현재 페이지를 대체합니다. 사용자가 뒤로 가기 버튼을 눌러 이전 페이지로 돌아갈 수 없습니다.

import { useRouter } from 'next/navigation';

const MyComponent = () => {
  const router = useRouter();

  const replacePage = () => {
    router.replace('/new-page');
  };

  return <button onClick={replacePage}>Replace with new page</button>;
};

router.replace의 인자

  • url: 이동할 경로 (문자열)
  • options: 추가 옵션 (선택 사항)
router.replace('/new-page', { shallow: true });

router.back

router.back는 사용자를 브라우저 히스토리에서 이전 페이지로 이동시킵니다. 이는 브라우저의 뒤로 가기 버튼을 누른 것과 동일한 효과를 가집니다.

import { useRouter } from 'next/navigation';

const MyComponent = () => {
  const router = useRouter();

  const goBack = () => {
    router.back();
  };

  return <button onClick={goBack}>Go back</button>;
};

요약

  • router.push(url, options): 사용자를 새로운 URL로 이동시키고, 히스토리에 새 항목을 추가합니다.
  • router.replace(url, options): 사용자를 새로운 URL로 이동시키고, 현재 페이지를 대체하여 히스토리에 새 항목을 추가하지 않습니다.
  • router.back(): 사용자를 브라우저 히스토리에서 이전 페이지로 이동시킵니다.