리뷰 등록하기

Minhyuk Song·2024년 12월 10일
0

쇼핑몰 기능 탐방

목록 보기
5/6
post-thumbnail

요구사항

  • 별을 클릭하여 별점을 남길 수 있다.
    • 클릭한 별에서 왼쪽 별들은 다 채워져야 한다.
  • 사진을 등록하고 미리 볼 수 있는 사진이 띄워져야 한다.
  • 상세 리뷰를 쓸 수 있는 textarea가 필요하다.

구현결과

1️⃣ 별점 리뷰

import { useState } from 'react';
import { FaHeart } from 'react-icons/fa';

const useStarScore = () => {
  const [score, setScore] = useState<number>(0);
  const ratingStarHandler = (): JSX.Element[] => {
    const reviewResult: JSX.Element[] = [];
    // ✅ 클릭한 별의 왼쪽부터 클릭한 별까지 하나씩 채우지기 위해 반복문 사용
    for (let i: number = 0; i < 5; i++) {
      reviewResult.push(
        <span
          key={i + 1}
          onClick={() => setScore(i + 1)}>
          {i + 1 <= score ? (
            <FaHeart className="h-[24px] w-[24px] cursor-pointer text-orange-600 transition-all duration-100 ease-in-out" />
          ) : (
            <FaHeart className="h-[24px] w-[24px] cursor-pointer text-gray-200 transition-all duration-100 ease-in-out dark:text-neutral-500" />
          )}
        </span>
      );
    }
    return reviewResult;
  };

  {/* ✅ 스코어라는 상태와 UI를 같이 리턴하기 위해 객체로 전달 */}
  return {![](https://velog.velcdn.com/images/be_matthewsong/post/8dc63cc3-82f7-43c9-b32d-a2711e62aaaf/image.gif)

    score,
    render: (
      <div className="flex flex-col gap-4">
        <h2 className="text-16 font-semibold">만족스러운 경험이었나요?</h2>
        <div className="flex gap-2">{ratingStarHandler()}</div>
      </div>
    )
  };
};

export default useStarScore;

2️⃣ 리뷰 내용 작성하기

리액트 훅 폼을 통해 비제어 컴포넌트로 입력값을 제어하는 방법도 있고, 여기서는 useRef를 통해 입력값을 가져오는 코드를 작성하겠습니다.

import { Button } from '@/components/ui/button';
import HeartReview from '@/features/review/useStarScore';
import {
  ChangeEvent,
  MutableRefObject,
  useCallback,
  useRef,
  useState
} from 'react';

const ReviewPage = () => {
  const [score, setScore] = useState<number>(0);

  const reviewContentRef = useRef<string>('');

  // ✅  useRef는 리렌더링을 유발하지 않고 값을 담을 수 있다.
  // 추후에 변경되지 않을 것 같아서 useCallback으로 함수에 대한 최적화 (메모이제이션)
  const handleChange = useCallback(
    (e: ChangeEvent<HTMLTextAreaElement>, ref: MutableRefObject<string>) => {
      const value = e.target.value;
      ref.current = value;
    },
    []
  );

  return (
    <div className="mt-10 flex h-full flex-col items-center justify-start gap-8">
      <HeartReview
        score={score}
        setScore={setScore}
      />
      // ✅ 적용한 곳
      <textarea
        className="mt-4 h-[200px] w-[400px] rounded-md border border-gray-300 p-4"
        placeholder="상품에 대한 리뷰를 20자 이상 작성해주세요"
        onChange={e => handleChange(e, reviewContentRef)}
      />

      <Button className="h-12 w-[400px] rounded-md bg-gray-800 text-white">
        리뷰 등록하기
      </Button>
    </div>
  );
};

export default ReviewPage;

3️⃣ 사진 등록하기

// features/review/usePhotoReview.tsx

import { useState, RefObject } from 'react';

// ✅ ref를 인자로 받아오고, ref로 들어온 이미지를 리더기로 url로 만드는 커스텀 훅
const usePhotoReview = (inputRef: RefObject<HTMLInputElement>) => {
  // ✅ 사진 URL가 담긴 상태
  const [photo, setPhoto] = useState<string | null>(null);

  // ✅ 사진 URL로 변환해주는 함수
  const handlePhotoChange = () => {
    if (inputRef.current && inputRef.current.files) {
      const file = inputRef.current.files[0];
      if (file) {
        const reader = new FileReader();
        reader.onload = e => {
          const dataUrl = e.target?.result;
          setPhoto(dataUrl as string);
        };
        reader.readAsDataURL(file);
      }
    }
  };

  return { photo, handlePhotoChange };
};

export default usePhotoReview;
import { Button } from '@/components/ui/button';
import usePhotoReview from '@/features/review/usePhotoReview';
import useStarScore from '@/features/review/useStarScore';
import { ChangeEvent, MutableRefObject, useCallback, useRef } from 'react';

const ReviewPage = () => {
  const reviewContentRef = useRef<string>('');
  // ✅ 이미지 파일을 받는 입력값에 액세스하기 위한 ref
  const inputRef = useRef<HTMLInputElement>(null);

  const handleChange = useCallback(
    (e: ChangeEvent<HTMLTextAreaElement>, ref: MutableRefObject<string>) => {
      const value = e.target.value;
      ref.current = value;
    },
    []
  );

  const { render: starScoreRender, score } = useStarScore();
  // ✅ 커스텀 훅으로 간단하게 호출 (추상화)
  const { photo, handlePhotoChange } = usePhotoReview(inputRef);

  return (
    <div className="mt-10 flex h-full flex-col items-center justify-start gap-8">
      <h1 className="text-xl font-bold">리뷰 작성하기</h1>
      {/* ✅ 아이콘을 클릭하여 이미지 파일 등록 받기 */}
      <img
        className="h-[200px] cursor-pointer"
        src={
          photo
            ? photo
            : `https://media.istockphoto.com/id/931643150/ko/%EB%B2%A1%ED%84%B0/%ED%94%BD%EC%B3%90-%EC%95%84%EC%9D%B4%EC%BD%98%ED%81%AC%EA%B8%B0.jpg?s=612x612&w=0&k=20&c=HLHcPbqrRiUjRJTNl3jZMRiwV8d-asY2_0dl19wn5_0=`
        }
        alt="리뷰 이미지"
        onClick={() => inputRef.current?.click()}
      />
      {/* ✅ 숨겨진 상태인 실제 이미지 인풋 */} 
      <input
        className="hidden"
        type="file"
        accept="image/*"
        ref={inputRef}
        onChange={handlePhotoChange}
      />
      {starScoreRender}
      <textarea
        className="mt-4 h-[200px] w-[400px] rounded-md border border-gray-300 p-4"
        placeholder="상품에 대한 리뷰를 20자 이상 작성해주세요"
        onChange={e => handleChange(e, reviewContentRef)}
      />

      <Button className="h-12 w-[400px] rounded-md bg-gray-800 text-white">
        리뷰 등록하기
      </Button>
    </div>
  );
};

export default ReviewPage;

배운 점

  • 라이브러리 없이 별점을 매기는 커스텀 훅을 만드는 게 상당히 고민하였는데, 실제로 구현하다 보면 완전 기초에 가까운 자바스크립트였던 것 같다.
  • 추상화를 하기 위한 커스텀 훅 패턴이 점점 늘어가는 기분이었다.
  • 입력값을 일반적으로 제어 컴포넌트로 하면 불필요한 리렌더링을 유발할 수 있어서 다른 비제어 컴포넌트 방식을 생각해보았다. (React-hook-form & useRef)
profile
어제보다 더 나은 오늘을 만들 수 있게

0개의 댓글

관련 채용 정보