[React] 최근 본 상품

강수정·2025년 4월 9일

🔎구현내용

DetailPage 클릭 하였을때 해당 상품 데이터를 localstorage에 저장하여 와이드 사이드바 및 마이페이지에 랜더링되도록 구현하고자한다.

localstroage 사용하는이유?
React의 state는 페이지가 새로고침되거나 브라우저가 닫히면 초기화되어 저장된 데이터가 사라진다. localStorage는 브라우저에 영구적인 데이터 저장이 가능하다. 새로고침하거나 브라우저를 다시 열더라도 데이터를 유지가 가능하기떄문에 최근본 상품 구현에 사용해보고자 한다.

DetailPage

detailPage 접속했을때 해당 아이템 담기


클릭한 상품들의 값이 담길 watchItem state를 빈배열로 생성하여 DetailPage가 처음 랜더링될때 해당 로직이 실행되도록한뒤
상품들의 상세페이지를 눌렀을 때 상품들의 stylecode를 LocalStorage에 저장하자

저장할떄는 localstorage.setItem를 사용하여 저장한다, 이떄 localstorage는 문자열만 저장이 가능하고 (객체,배열)형태는 저장이 불가능하다 JSON.stringify 을사용하여 반드시 문자열로 변환하여 저장하자

가져올때는 배열로 다시 가져와야하기떄문에 JSON.parse 을 사용하여 변환한뒤 localstorage.getItem을 사용하여 가져오자

set 함수를 사용하여 중복된 아이템을 제거하여 같은 stylecode를 기준으로 한번만 저장되도록하자

📍localstorage 문법

  • localStorage.getItem('key') : key값을 가져옴
  • localstorage.setItem('key', value) : key에 문자열 형태로 데이터를 저장
  • localstorage.removeItem('key') :해당 key에 저장된 항목을 삭제

📍json 문법

  • Set함수 : 중복 제거
  • Array.from() : 배열로 다시 변환할 때 사용
  • unshift : 배열의 맨 앞에 새 값을 추가 (기존 값은 뒤로 밀림)
  • JSON.parse(): 문자열을 다시 원래 형태(배열/객체) 로 변환
  • JSON.stringify() : 객체나 배열을 문자열로 변환해서 저장 가능하게 함

export default function DetailPage (){

      let [watchItem, setWatch] = useState([]);

        useEffect(() => {
        let watch = JSON.parse(localStorage.getItem('watched')) || [];

        watch.unshift(stylecode); // 최신 상품이 앞에 위치하도록 추가
        watch = Array.from(new Set(watch)); // 중복 제거

        localStorage.setItem('watched', JSON.stringify(watch));
        setWatch(watch);
    }, []);

	return(
    	<>
        	{...기타코드생략}
        </>
    )

}

RecentSection

최근본상품 마이페이지 랜더링


마이페이지 > 최근본상품 컴포넌트에서 useEffect 메서드를 사용하여 watched에 저장되어있는 로컬스토리지의 값을 가져온뒤 새로운 state를 let으로 선언하여 생성한뒤 해당 state에 해당값을 저장하자

customhook으로 생성한 상품데이터 allItems를 가져온뒤 해당 데이터에서 stylecode에 해당하는 배열 전체(color)를 가져온다. 그리고 부모 배열의 id만 추출하여 디테일페이지로 이동할수있도록 설정하자

📁item structure

{
  id: 1 //부모배열
  colors:{
      color:{
          stylecode: ''
          {...기타코드생략}
      },
      color:{
          stylecode: ''
          {...기타코드생략}
      },
      color:{
          stylecode: ''
          {...기타코드생략}
      }  
  }
}
import React, { useState, useEffect } from 'react';
import useAllProducts from '../../../hooks/useAllProducts';
import styles from '../../../assets/styles/mypage.module.css';
import { Link } from 'react-router-dom';

export default function RecentSection() {
    let [watchItem, setWatch] = useState([]);
    const allItems = useAllProducts();

     useEffect(() => {
            let watched = JSON.parse(localStorage.getItem('watched')) || [];
            setWatch(watched);
        }, []);
        
        
        //필터
         const matchedItems = watchItem
        .map((code) => {
            const parent = allItems.find((item) => (item.colors || []).some((color) => color.stylecode === code));
            if (!parent) return null;

            const color = parent.colors.find((color) => color.stylecode === code);
            return color ? { ...color, parentId: parent.id } : null;
        })
        .filter(Boolean);

    return (
        <div className={styles.recentWrapper}>
            <ul>
                {matchedItems.map((item, index) => (
                    <li
                        key={index}
                        className={styles.recentItem}
                    >
                        <Link to={`/detail/${item.parentId}/${item.stylecode}`}>
                            <div className={styles.imgBox}>
                                <img
                                    src={item.imgUrl[0].Main}
                                    alt={item.koname}
                                />
                            </div>
                            <div>
                                <p>{item.koname}</p>
                            </div>
                        </Link>
                    </li>
                ))}
            </ul>
        </div>
    );

}

Sidebar

홈페이지의 오른쪽 사이드바에 최근본상품이라는 고정사이드바를 구현하고자한다.
이전 로직과 동일하지만 해당 고정사이드바는 동적으로 새로고침없이 실시간으로 해당 내용이바뀌도록 설정해야한다.

기존 DetailPage 컴포넌트에서 새로운 클릭 이벤트를 감지하기 위해 커스텀이벤트 리스너 (dispatchEvent)를 생성하여 key값을 정의한다.
사이드바에서 useEffect 마운팅시, 해당 key값의 이벤트가 발생했을때watchItem가 랜더링되도록 window.addEventListener ('key', 실행할_함수) 이벤트를 적용하자
map으로 매핑시 slice(0,2)를 적용하여 최근2개의 제품만 나올수있도록 랜더링

//detailPage.js

    useEffect(() => {
        window.dispatchEvent(new Event('updateWatched'));
        //이벤트 감지 정의 
		{...기타 코드생략}
    }, []);
//sidebar.js
    useEffect(() => {
        const handleUpdate = () => {
            const watched = JSON.parse(localStorage.getItem('watched')) || [];
            setWatch(watched);
        };

        window.addEventListener('updateWatched', handleUpdate);

		handleUpdate(); //처음 마운팅 될때도 실행 
    }, []);

전체삭제

로컬스트르지, state 둘다 초기화되도록 설정하면 끝

  const handleDeleteAll = () => {
		{..기타 코드생략} 
        localStorage.removeItem('watched');
        setWatch([]);
    };
    
        return (
			{...기타코드생략}
            <section>
                <button onClick={handleDeleteAll}>전체삭제</button>
            </section>
		)

선택삭제

  • 선택삭제 버튼 클릭시 선택 오버레이가 나타나고 한번 더 토글시 선택 오버레이가 닫힌다.

  • 체크박스 클릭시 checkedItems에 상품 stylecode가 배열속에 담긴다.

  • 삭제 버튼 클릭시 선택한 상품들을 제외한 아이템들을 남겨두는 핸들러를 생성

    (체크 아이콘은 utils에서 따로 생성한 CheckButton 컴포넌트를 사용 해당부분 생략)

export default function RecentSection (){
	const [onCheck, setOnCheck] = useState(false) // 체크박스 오버레이 상태 
    const [checkedItems, setCheckedItems] = useState([]) //체크리스트
	
    //오버레이 토글 핸들러
    const handleToggleCheck = () => {
    	setOnCheck(!onCheck)
    }
    
    //stylecode 업데이트 핸들러
    const handleUpdateItmes = (checkStyleCode) => {
	    setCheckedItems(checkStyleCode)
    }
    
    //선택사제 핸들러
    const handleDeleteEl = () => {
    	if(checkedItems.length === 0){
        	alert('삭제할 상품을 선택해주세요')
            return;
        }
        const confrim = window.confirm (`선택한 ${checkedItems.length}개의 최근 본상품을 삭제하시겠습니까?`)
        
        if(!confirim){
        	return
        }
        
        const watchedData = JSON.parse(localStorage.getItem('watched')) || [];
	    const updatedData = watchedData.filter((item) => !checkedItems.includes(item));
        localStorage.setItem('watched', JSON.stringify(updatedData));

        setWatch(updatedData);
        setCheckItems([]); // 선택 초기화
    }
}


return(
	{...기타부분생략}
    <div>
    	<section>
        	<button onClick={handleToggleCheck}>{onCheck ? '선택삭제' : '선택해제'}</button>
            <button onCLick={handleDeleteEl(item.stylecode)}>삭제</button>
        </sectin>
        
        <ul>
        	   {matchedItems.map((item, index) => (
               	  <li
                            key={index}
                   >
                   	Link
                          to={`/detail/${item.parentId}/${item.stylecode}`}
                          onClick={(e) => {
                           if (onCheck) {
                                        e.preventDefault();
                           }
                                }}
                            >
                                <div className={styles.imgBox}>
                                    {onCheck && (
                                        <div className={styles.overlay}>
                                            <CheckButton onClick={() => handleCheckItems(item.stylecode)} />
                                        </div>
                                    )}
                                    <img
                                        src={item.imgUrl[0].Main}
                                        alt={item.koname}
                                    />
                                </div>
                            </Link>
                   
                   
                   
                   </li>
               
               )
        </ul>
    </div>
)

고정사이드바 업데이트

전체삭제 및 선택삭제한 결과를 고정사이드바에서도 업데이트 하기위해 똑같이
삭제 핸들러에 아래코드를 추가하면 된다.

        window.dispatchEvent(new Event('updateWatched'));

고정사이드바 컴포넌트에서는 removeEventListener를 추가하면 끝

    return () => {
            window.removeEventListener('updateWatched', handleUpdate);
        };

끝마치며

지금까지 디테일페이지 해당하는 상품 클릭시, 최근본상품에 추가하는 로직만을 구현했다. 추후에 MainPage의 상품 클릭시 해당 정보를 추가하는 로직과 sidebar, recentsection 컴포넌트의 filter 재사용을 위한 utils를 생성하여 한곳에 모아 리팩토링할 예정이다

0개의 댓글