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 {
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;
리액트 훅 폼을 통해 비제어 컴포넌트로 입력값을 제어하는 방법도 있고, 여기서는 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;
// 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;