somn45 / mucketlist

React, Typescript, Spotify API를 이용해 좋아하는 장르의 곡을 지속적으로 추적, 몰랐던 명곡을 저장할 수 있는 APP
0 stars 0 forks source link

Track state의 변화에 따라 리랜더링 수행 도중 무한 루프? 에러 메세지 출력 발생 #2

Closed somn45 closed 2 years ago

somn45 commented 2 years ago

장르 선택, 그리고 트랙의 순서의 결정 이후 트랙을 불러오는 과정에서 다음 에러 메시지가 발생합니다.

Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either 
doesn't have a dependency array, or one of the dependencies changes on every render.

에러 메시지를 해석해보면 최대 업데이트 깊이를 초과했으며 useEffect 안에서 setState를 사용할 때 종속성 배열이 존재하지 않거나 모든 랜더링에서 종속성 중 하나가 변경된다는 것으로 해결이 가능하다고 나와있습니다. 다음은 에러 메시지의 원인인 코드 내용입니다.

// Home.tsx
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Cookies } from 'react-cookie';
import axios from 'axios';
import { connect } from 'react-redux';
import Genre from '../components/Genre';
import Settings, { TrackState } from './Settings';
import Tracks from './Tracks';

interface HomeProps {
  selectedGenres: string[];
}

const cookies = new Cookies();

function Home({ selectedGenres }: HomeProps) {
  console.log('home');
  const navigate = useNavigate();
  const [genres, setGenres] = useState<string[]>([]);
  const [tracks, setTracks] = useState<TrackState[]>([]);
  const [sortedTracks, setSortedTracks] = useState<TrackState[]>([]);
  const [loading, setLoading] = useState(true);
  const [isOpenSettings, setIsOpenSettings] = useState(false);
  useEffect(() => {
    getSpotifyGenres();
    if (!cookies.get('F_UID')) navigate('/login');
  }, []);

  useEffect(() => {
    const tracks = localStorage.getItem('tracks');
    if (!tracks) return;
    setSortedTracks(JSON.parse(tracks));
  }, [sortedTracks]);

  const getSpotifyGenres = async () => {
    const accessToken = cookies.get('accessToken');
    const response = await axios.post(`http://localhost:3001/genres`, {
      accessToken: accessToken,
    });
    setGenres(response.data.genres);
    setLoading(false);
    setIsOpenSettings(true);
  };

  const onClick = async (e: React.MouseEvent<HTMLInputElement>) => {
    e.preventDefault();
    if (selectedGenres.length === 0) {
      return;
    }
    console.log(selectedGenres);
    const genres = JSON.stringify(selectedGenres);
    const accessToken = cookies.get('accessToken');
    const response = await axios.get(
      `http://localhost:3001/search?accessToken=${accessToken}&genre=${genres}`
    );
    console.log(response);
    setTracks(response.data.tracks);
  };
  return (
    <div>
      <h2>Home</h2>
      <form>
        {loading ? (
          <h2>장르 목록을 불러오는 중입니다.</h2>
        ) : (
          <>
            {genres.map((genre) => (
              <Genre key={genre} genre={genre} />
            ))}
          </>
        )}
        <input
          type="submit"
          value="Search your favorite genres"
          onClick={onClick}
        />
      </form>
      {isOpenSettings ? <Settings tracks={tracks} /> : null}
      {sortedTracks ? <Tracks tracks={sortedTracks} /> : null}
    </div>
  );
}

function mapStateToProps(state: string[]) {
  return { selectedGenres: state };
}

export default connect(mapStateToProps)(Home);

우선 에러 메세지에 적혀 있듯이 해결 방법은 이미 알아낸 상태입니다. 그러나 저희가 원하는 것은 장르, 트랙의 순서 결정 이후 브라우저의 새로고침 없이 트랙 리스트를 곧바로 브라우저에 보여주는 기능을 제작하는 것이 목적입니다. 에러 메시지대로 문제를 해결하게 되면 브라우저의 새로고침 이후에 트랙 리스트를 확인할 수 있습니다. 혹시 다른 대안이 없는지 궁금합니다.

somn45 commented 2 years ago

해결했습니다! 원인은 useEffect 안에 setState 때문이었습니다. useEffect의 의존성 배열과 setState에서 set하려는 상태가 서로 같을 때 상태가 변하면 useEffect가 동작하고 또 그안에 있는 setState 때문에 상태가 변해서 useEffect가 동작하는 무한반복성 동작이 일어나게 됩니다. 아래의 코드로 더 쉽게 이해하실 수 있습니다.

  useEffect(() => {
    const tracks = localStorage.getItem('tracks');
    if (!tracks) return;
    setSortedTracks(JSON.parse(tracks));
  }, [sortedTracks]);

의존성 배열에 들어가는 상태를 다른 변수로 교체함으로써 해결했습니다. Mucketlist 앱에서는 tracks의 장르를 결정한 후 tracks의 순서를 결정짓는 설정을 완료할 때 track이 브라우저에 보여주는 방식입니다. 따라서 setting을 상태로 만들어 의존성 배열에 배치하여 위의 무한반복성 동작을 해결했습니다.

function Home({ settings }) {
  useEffect(() => {
    const tracks = localStorage.getItem('tracks');
    if (!tracks) return;
    setSortedTracks(JSON.parse(tracks));
  }, [settings]);
...
const mapStateToProps = (state: HomeStates) => {
  return {
    settings: state.settings,
  };
}
export default connect(mapStateToProps)(Home);