axios 응답 데이터 중첩 관련 질문이요 ㅠ

일단 오류는 이렇습니다 ㅠ

먼저 구현 하려는 기능은 통신해서 가져온 데이터를 합쳐 랜더링 시키는 것인데요(무한스크롤)

키프롭이 중복된다고 오류가 뜨네요… 분명 중복되는 데이터는 없다는 것을 확인했는데 어떤 코드가 잘못된 것인지

제가 리덕스를 잘못 이해하고 있는 것인지 …로컬스토리지를 이해 못한 것인지 ㅠㅠ 시간 나시면 한번 봐주실 수

있을까요? ㅠㅠ 신규는 이미지 한개만 올릴 수 있다해서 코드로 올리겠습니다 ㅠ

일단 골격은 앱에서 서치바와 갤러리 컨테이너 랜더링

const [searchState, setSearchState] = useState(false);
  // searchState의 값에 따라 메인 화면과 검색결과 화면의 전환이 이루어짐.
  const [userInfo, setUserInfo] = useState({
    isLogin: false,
    accessToken: ''
  });

  const getToken = async (authorizationCode) => {
    const { access_token } = await utils.getAccessToken(authorizationCode);
    if(!access_token) return
    setUserInfo({
      ...userInfo,
      isLogin: true,
      accessToken: access_token
    })
    localStorage.setItem('access_token', access_token);
    window.history.replaceState({}, null, window.location.pathname)
  }

  useEffect(() => {
    const url = new URL(window.location.href);
    const authorizationCode = url.searchParams.get('code');
    const token = localStorage.getItem('access_token');

    if(token) {
      setUserInfo({
        ...userInfo,
        isLogin: true,
        accessToken: token
      })
      return;
    }
    if(!authorizationCode && !token) return
    if(authorizationCode) {
      getToken(authorizationCode)
      return
    }
    
  }, [])

  return (
    <Container>
      <Header userInfo={userInfo} setUserInfo={setUserInfo}/>
      <Search setSearchState={setSearchState} />
      <Gallery searchState={searchState} />
    </Container>
  )

여기에서는 검색 인풋에 값이 있고 검색버튼 누르면 그에 따른 결과를 보여 줍니다 여기에 무한스크롤 관련이랑

api호출 관련 로직이 있습니다.

const Gallery = ({ apiData, searchState }) => {
  const { loading, data, error } = useSelector(state => state.dataReducer);
  const dispatch = useDispatch();
  // const [hasNext, setHasNext] = useState(true);
  const getMoreImgEl = useRef(null);
  const intersecting = useInfiniteScroll(getMoreImgEl);
  const [page, setPage] = useState(1);
  // console.log(page)

 useEffect(()=> {
    dispatch(getImgs(page));
    setPage(page => page + 1);
  }, [dispatch, intersecting])
if(loading) return <Loading>Loading.....</Loading>;
  if(error) return <Error>Error!!</Error>



  return (
      <GalleryContainer>
        {
          searchState ? <SearchCard /> : <CardList apiData={data} loading={loading} error={error}/>
        }
        {
          !searchState && <div ref={getMoreImgEl}/>
        }
        {/* <div ref={getMoreImgEl}/> */}
      </GalleryContainer>
  )
}

이것은 리덕스 코드 입니다

import axios from "axios";

const REQ_DATA = 'images/REQ_DATA';
const REQ_DATA_SUCCESS = 'images/REQ_DATA_SUCCESS';
const REQ_DATA_ERROR = 'images/REQ_DATA_ERROR';


const initialState = {
  loading: false,
  data: null,
  error: null,
}

export const getImgs = (page) => async (dispatch, getState) => {
  const localData = localStorage.getItem('data') || [];
  let { storeData } = getState().dataReducer;
  const API = `https://api.unsplash.com/photos/?client_id=${process.env.REACT_APP_ACCESS_KEY}&page=${page}&per_page=20`
if(localData.length === 0) {
    dispatch({ type: REQ_DATA })
    try {
      const { data } = await axios.get(API)
      const payload = data;
      console.log(payload)
      localStorage.setItem('data', JSON.stringify(payload))
  
      dispatch({ type: REQ_DATA_SUCCESS, payload })
    } catch(e) {
      const payload = e;
      dispatch({ type: REQ_DATA_ERROR, payload })
    }
  } else {
    const prevData = [...JSON.parse(localData)];
    console.log('heoolo?')
    console.log(page)
    try {
      const { data } = await axios.get(API);
      console.log(data);
      console.log(prevData);
      const payload = [...prevData, ...data];
      console.log(payload)
      dispatch({ type: REQ_DATA_SUCCESS, payload })
    } catch(e) {
      const payload = e;
      dispatch({ type: REQ_DATA_ERROR, payload })
    }
    
  }
  
}

const dataReducer = (state = initialState, action) => {
  switch(action.type) {
    case REQ_DATA:
      return {
        ...state,
        loading: true,
        error: null,
      }
    case REQ_DATA_SUCCESS:
      return {
        ...state,
        loading: false,
        data: action.payload,
      }
    case REQ_DATA_ERROR:
      return {
        ...state,
        loading: false,
        data: null,
        error: action.payload
      }
    default:
      return state;
  }
}

혹시 몰라 무한스크롤 관련 코드도 올립니다 .

import { useEffect, useState, useRef, useCallback } from "react";

const useInfiniteScroll = (targetEl) => {
  const observerRef = useRef(null);
  const [intersecting, setIntersecting] = useState(false);
  // const observer = new IntersectionObserver(entries => setIntersecting(entries.some(entry => entry.isIntersecting)));

  const getObserver = useCallback(() => {
    if(!observerRef.current) {
      observerRef.current = new IntersectionObserver(entries => setIntersecting(entries.some(entry => entry.isIntersecting)));
    }
    return observerRef.current;
  }, [observerRef.current])

  useEffect(() => {
    if(targetEl.current) {
      getObserver().observe(targetEl.current);
    }

    return () => {
      getObserver().disconnect();
    }
  }, [getObserver, targetEl.current]);  
  return intersecting;
}

export default useInfiniteScroll; 

키프롭이 상당히 독특하네요. 혹시 css-in-js 라이브러리 쓰시지 않나요?
랜덤으로 저렇게 독특한 겹치지 않는 클라스 네임을 만들어내곤 하던데.

좋아요 1

넵 styled-components 사용하고 있습니다.!

아마 저 키가 unsplash api에서 받은 res의 개별 id값일거에요?? 아닐까요??

제가 잘못 생각 하고 있는건가요??

map으로 반복 랜더링 하면 고유 키값이 필요해 저렇게 넣었는데 res 의 id가 문제가 아니라 스타일드 컴포넌트 문제인가요??

안녕하세요!! 반갑습니다. :raised_hand:

우선 코드를 자세히 살피지는 못했지만 우선 이런 무한 스크롤에서 많이 하는 실수 두가지를 적어보겠습니다.

  1. 1페이지를 불러오고 스크롤 할 때 2페이지를 불러오는 요청이 여러번 호출된다.
  2. 다음 페이지를 불러온 아이템들과 기존에 불러온 아이템들을 합칠 때 id가 중복된 것이 발생할 수 있는데 이에 대한 처리가 없다.
  • 위 1번에서처럼 여러번 호출되게 하는 실수를 하지 않았다 하더라도 처음 1페이지(9,8,7,6,5 번글)를 불러오고 2페이지(4,3,2,1,0 번글)를 불러오기 전에 새로운 아이템(10번글)이 추가되게 되면 1페이지에 있던 5번글이 2페이지로 넘어 옵니다. 따라서 2페이지를 불러오면 기본에 불러온 1페이지(9,8,7,6,5)번글의 5번이 둘다 존재하게 됩니다.

그리고 참고로 더 말씀드리자면 react를 시작한지 얼마 안되셨다면 api의 결과를 글로벌로 관리하려고 할 때 redux를 쓰기보다는 react-query 같은 것을 써보시는 것을 추천합니다. 저도 한때 redux 팬이었지만 지금은 사용하지 않고 있습니다. 참고로 저는 요즘 relay(w/ graphql)을 쓰고 있습니다만 이는 graphlq을 사용할 경우이니 rest api의 데이터 관리는 react-query로 그외 global state는 recoil을 써보세요~ :pray::+1:

useReducer는 추천않하시나요 ?

좋아요 1

이미 아시고 질문하셨을 수 있는데 useReducer 자체적으로 redux나 react-query 처럼 전역으로 상태를 관리하는 기능은 없습니다.
어떠한 action을 정의하고 그에 따른 여러 상태가 바뀌어야하는 경우에는 useReducer를 사용하는게 적절합니다. 특히 하나의 비동기 액션이 여러 상태를 변경하게 될 경우 불필요한 렌더링을 useReducer를 이용해서 방지할 수 있습니다. (사용 예)

그런데 아주 복잡하게 상태가 엮여있지 않는 한 개인적으로는 그냥 별개의 state를 두고 작업하게 되더군요. react 18 버전부터 여러 상태 변화를 자동으로 한번에 batch해 주기도하고 코드 작성할 때나 읽을 때 더 간결하다고 느껴졌습니다.

:pray: