분류 Reactjs

React에서 Spotify 음악 검색 앱을 만드는 방법

컨텐츠 정보

  • 조회 792 (작성일 )

본문

소개 


이 기사에서는 Spotify Music API를 사용하여 완전 반응 형 Spotify 음악 검색 앱을 만듭니다.


https://dev.to/myogeshchavan97/how-to-create-a-spotify-music-search-app-in-react-328m


이 앱을 만들면, 다음을 배우게 됩니다.


  1. Spotify API를 사용하여 OAuth 인증을 제공하는 방법
  2. 앨범, 아티스트 및 재생 목록을 검색하는 방법
  3. 아름다운 UI로 세부 사항 표시
  4. 목록에서 직접 노래 재생
  5. 앱에 더 많은 기능 로드를 추가하는 방법
  6. 앨범, 아티스트 및 재생 목록에 대해 별도의 로드 기능을 추가하고 유지하는 방법

그리고 훨씬 더.


아래 비디오에서 최종 작업 응용 프로그램의 라이브 데모를 볼 수 있습니다.


초기 설정 


create-react-app을 사용하여 새 프로젝트를 만듭니다.


create-react-app spotify-music-search-app


프로젝트가 생성되면 src 폴더에서 모든 파일을 삭제하고 src 폴더 안에 index.js 및 styles.css 파일을 생성합니다. 

또한 src 폴더 내에 작업, 구성 요소, 이미지, 감속기, 라우터, 저장소 및 utils 폴더를 만듭니다.


필요한 종속성을 설치하십시오.


yarn add axios@0.19.2 bootstrap@4.5.2 lodash@4.17.19 prop-types@15.7.2 react-bootstrap@1.3.0 redux@4.0.5 react-redux@7.2.1 react-router-dom@5.2.0 redux-thunk@2.3.0


styles.css를 열고 여기에서 내용을 추가하십시오.


초기 페이지 생성 


음 내용으로 구성 요소 폴더 내에 새 파일 Header.js를 만듭니다.


import React from 'react';
const Header = () => {
  return <h1 className="main-heading">Spotify Music Search</h1>;
};
export default Header;


다음 내용으로 구성 요소 폴더 내에 새 파일 RedirectPage.js를 만듭니다.


import React from 'react';
const RedirectPage = () => {
 return <div>Redirect Page</div>;
};
export default RedirectPage;   


다음 내용으로 구성 요소 폴더 내에 새 파일 Dashboard.js를 만듭니다.


import React from 'react';
const Dashboard = () => {
 return <div>Dashboard Page</div>;
};
export default Dashboard;



다음 내용으로 구성 요소 폴더 내에 새 파일 Home.js를 만듭니다.


import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
import Header from './Header';
const Home = (props) => {
  return (
    <div className="login">
      <Header />
      <Button variant="info" type="submit">
        Login to spotify
      </Button>
    </div>
  );
};
export default connect()(Home);


다음 내용으로 구성 요소 폴더 내에 새 파일 NotFoundPage.js를 만듭니다.


import React from 'react';
import { Link } from 'react-router-dom';
import Header from './Header';
const NotFoundPage = () => {
  return (
    <React.Fragment>
      <Header />
      Page not found. Goto <Link to="/dashboard">Home Page</Link>
    </React.Fragment>
  );
};
export default NotFoundPage;


다음 내용으로 라우터 폴더 내에 새 파일 AppRouter.js를 만듭니다.


import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from '../components/Home';
import RedirectPage from '../components/RedirectPage';
import Dashboard from '../components/Dashboard';
import NotFoundPage from '../components/NotFoundPage';
class AppRouter extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <div className="main">
          <Switch>
            <Route path="/" component={Home} exact={true} />
            <Route path="/redirect" component={RedirectPage} />
            <Route path="/dashboard" component={Dashboard} />
            <Route component={NotFoundPage} />
          </Switch>
        </div>
      </BrowserRouter>
    );
  }
}
export default AppRouter;


여기서는 react-router-dom 라이브러리를 사용하여 Home Page, Dashboard Page, Not Found Page, Redirect Page와 같은 다양한 페이지에 대한 라우팅을 설정했습니다.


다음 내용으로 reducers 폴더 안에 새 파일 albums.js를 만듭니다.


const albumsReducer = (state = {}, action) => {
  switch (action.type) {
    default:
      return state;
  }
};
export default albumsReducer;


다음 내용으로 reducers 폴더 내에 새 파일 artists.js를 작성하십시오.


const artistsReducer = (state = {}, action) => {
  switch (action.type) {
    default:
      return state;
  }
};
export default artistsReducer;



다음 내용으로 reducers 폴더에 새 파일 playlist.js를 만듭니다.


const playlistReducer = (state = {}, action) => {
  switch (action.type) {
    default:
      return state;
  }
};
export default playlistReducer;


위의 모든 감속기에서 기본 상태로 reducers를 설정했습니다. 앱을 진행하면서 더 많은 스위치 케이스를 추가 할 예정입니다.


다음 내용으로 store 폴더 내에 새 파일 store.js를 만듭니다.


import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import albumsReducer from '../reducers/albums';
import artistsReducer from '../reducers/artists';
import playlistReducer from '../reducers/playlist';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  combineReducers({
    albums: albumsReducer,
    artists: artistsReducer,
    playlist: playlistReducer
  }),
  composeEnhancers(applyMiddleware(thunk))
);

export default store;



여기에서는 AppRouter.js 파일에 정의 된 모든 구성 요소에서 저장소 데이터에 액세스 할 수 있도록 모든 reducer가 결합 된 redux 저장소를 만들었습니다.


이제 src / index.js 파일을 열고 그 안에 다음 내용을 추가합니다.


import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store/store';
import AppRouter from './router/AppRouter';
import 'bootstrap/dist/css/bootstrap.min.css';
import './styles.css';

ReactDOM.render(
  <Provider store={store}>
    <AppRouter />
  </Provider>,
  document.getElementById('root')
);


여기에서는 Redux 저장소를 AppRouter 구성 요소에 선언 된 모든 경로에 전달할 Provider 구성 요소를 추가했습니다.


이제 터미널에서 다음 명령을 실행하여 React App을 시작합니다.


yarn start



http : // localhost : 3000 /에서 애플리케이션에 액세스하면 다음 화면이 표시됩니다.


Login Screen 


로그인 인증 기능 추가 


이제 로그인 기능을 추가해 보겠습니다. 앱을 사용하여 Spotify 계정에 로그인하려면 client_id, authorize_url 및 redirect_url의 세 가지가 필요합니다.


여기로 이동하여 Spotify 개발자 계정에 로그인하십시오 (계정이 없는 경우 가입).


로그인 후 아래 화면과 비슷한 화면이 나옵니다.


Account Dashboard 


앱 만들기 녹색 버튼을 클릭하고 앱 이름과 설명을 입력 한 다음 만들기 버튼을 클릭합니다.


Create App 


생성 된 클라이언트 ID를 기록해 둡니다.

Client ID 


그런 다음 설정 편집 버튼을 클릭합니다. Redirect URIs의 값으로 http : // localhost : 3000 / redirect를 입력하고 ADD 버튼을 클릭 한 다음 조금 스크롤하여 SAVE 버튼을 클릭합니다.


Edit Settings 


이제 프로젝트의 루트에 .env라는 이름의 새 파일을 만들고 그 안에 다음 세부 정보를 추가합니다.


REACT_APP_CLIENT_ID=your_client_id
REACT_APP_AUTHORIZE_URL=https://accounts.spotify.com/authorize
REACT_APP_REDIRECT_URL=http://localhost:3000/redirect


  • REACT_APP_AUTHORIZE_URL은 앱에서 Spotify 계정에 액세스하기 위한 승인 팝업을 표시하는 데 사용됩니다.
  • REACT_APP_REDIRECT_URL은 사용자가 성공적으로 승인되면 사용자를 리디렉션 할 URL입니다.
  • 각 변수는 REACT_APP_로 시작하므로 Create React App은 자동으로 해당 변수를 process.env 객체에 추가하여 애플리케이션에서 액세스 할 수 있도록 합니다.

.env 파일을 .gitignore 파일에 추가하여 공개해서는 안되는 개인 정보를 포함하고 있으므로 git에 추가되지 않도록 하십시오.


REACT_APP_REDIRECT_URL 변수의 값은 위에 표시된 설정 편집 스크린 샷에서 리디렉션 URI에 입력 한 값과 일치해야 합니다. 그렇지 않으면 응용 프로그램이 작동하지 않습니다.


이제 src / components / Home.js를 열고 onClick 핸들러를 로그인 버튼에 추가하십시오.


<Button variant="info" type="submit" onClick={handleLogin}>
  Login to spotify
</Button>



그리고 handleLogin 함수를 추가하십시오.


const {
  REACT_APP_CLIENT_ID,
  REACT_APP_AUTHORIZE_URL,
  REACT_APP_REDIRECT_URL
} = process.env;

const handleLogin = () => {
  window.location = `${REACT_APP_AUTHORIZE_URL}?client_id=${REACT_APP_CLIENT_ID}&redirect_uri=${REACT_APP_REDIRECT_URL}&response_type=token&show_dialog=true`;
};



업데이트 된 Home.js 파일은 다음과 같습니다.


import React from 'react';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
import Header from './Header';
const Home = (props) => {
  const {
    REACT_APP_CLIENT_ID,
    REACT_APP_AUTHORIZE_URL,
    REACT_APP_REDIRECT_URL
  } = process.env;
  const handleLogin = () => {
    window.location = `${REACT_APP_AUTHORIZE_URL}?client_id=${REACT_APP_CLIENT_ID}&redirect_uri=${REACT_APP_REDIRECT_URL}&response_type=token&show_dialog=true`;
  };
  return (
    <div className="login">
      <Header />
      <Button variant="info" type="submit" onClick={handleLogin}>
        Login to spotify
      </Button>
    </div>
  );
};
export default connect()(Home);


이제 터미널에서 yarn start 명령을 실행하여 앱을 시작하고 로그인 기능을 확인하십시오.


Login Authentication 



보시다시피 AGREE 버튼을 클릭하면 RedirectPage 구성 요소로 리디렉션되고 Spotify는 아래와 같이 리디렉션 URL에 access_token, token_type 및 expires_in을 자동으로 추가합니다.


http://localhost:3000/redirect#access_token=BQA4Y-o2kMSWjpRMD5y55f0nXLgt51kl4UAEbjNip3lIpz80uWJQJPoKPyD-CG2jjIdCjhfZKwfX5X6K7sssvoe20GJhhE7bHPaW1tictiMlkdzkWe2Pw3AnmojCy-NzVSOCj-aNtQ8ztTBYrCzRiBFGPtAn-I5g35An10&token_type=Bearer&expires_in=3600


  • access_token은 나중에 Spotify API에 대한 모든 요청에 ​​추가 할 Bearer 토큰입니다.
  • expires_in은 3600 초, 즉 기본적으로 1 시간 인 토큰 만료 시간을 지정합니다. 그 후에 다시 로그인해야 합니다.

검색 기능 추가 


이제 토큰에 액세스 할 수 있으므로 모든 API 요청에 사용할 수 있도록 어딘가에 저장해야 합니다.


다음 내용으로 src / utils 폴더에 functions.js라는 이름의 새 파일을 만듭니다.


import axios from 'axios';
export const getParamValues = (url) => {
  return url
    .slice(1)
    .split('&')
    .reduce((prev, curr) => {
      const [title, value] = curr.split('=');
      prev[title] = value;
      return prev;
    }, {});
};
export const setAuthHeader = () => {
  try {
    const params = JSON.parse(localStorage.getItem('params'));
    if (params) {
      axios.defaults.headers.common[
        'Authorization'
      ] = `Bearer ${params.access_token}`;
    }
  } catch (error) {
    console.log('Error setting auth', error);
  }
};


여기에 추가했습니다.


  • 다음과 같은 객체에 access_token, token_type 및 expires_in 값을 저장하는 getParamValues ​​함수 :


{
 access_token: some_value,
 token_type: some_value,
 expires_in: some_value
}


  • 모든 axios API 요청에 access_token을 추가하는 setAuthHeader 함수

RedirectPage.js 파일을 열고 다음 내용으로 바꿉니다.


import React from 'react';
import _ from 'lodash';
import { getParamValues } from '../utils/functions';
export default class RedirectPage extends React.Component {
  componentDidMount() {
    const { setExpiryTime, history, location } = this.props;
    try {
      if (_.isEmpty(location.hash)) {
        return history.push('/dashboard');
      }
      const access_token = getParamValues(location.hash);
      const expiryTime = new Date().getTime() + access_token.expires_in * 1000;
      localStorage.setItem('params', JSON.stringify(access_token));
      localStorage.setItem('expiry_time', expiryTime);
      history.push('/dashboard');
    } catch (error) {
      history.push('/');
    }
  }
  render() {
    return null;
  }
}


여기에서는 URL 매개 변수에 액세스하고 로컬 저장소에 저장하는 componentDidMount 수명주기 메서드를 추가했습니다. location.hash에서 사용 가능한 URL 값을 전달하여 getParamValues ​​함수를 호출합니다.


expires_in 값은 초 단위 (& expires_in = 3600)이므로 1000을 곱한 다음 현재 시간의 밀리 초에 더하여 밀리 초로 변환합니다.

const expiryTime = new Date().getTime() + access_token.expires_in * 1000;


따라서 expiryTime에는 토큰 생성 시간 1 시간 후의 시간의 밀리 초가 포함됩니다 (expires_in은 3600이므로).


다음 내용으로 utils 폴더에 새 파일 constants.js를 만듭니다.


export const SET_ALBUMS = 'SET_ALBUMS';
export const ADD_ALBUMS = 'ADD_ALBUMS';
export const SET_ARTISTS = 'SET_ARTISTS';
export const ADD_ARTISTS = 'ADD_ARTISTS';
export const SET_PLAYLIST = 'SET_PLAYLIST';
export const ADD_PLAYLIST = 'ADD_PLAYLIST';


다음 내용으로 작업 폴더 내에 새 파일 result.js를 만듭니다.


import {
  SET_ALBUMS,
  ADD_ALBUMS,
  SET_ARTISTS,
  ADD_ARTISTS,
  SET_PLAYLIST,
  ADD_PLAYLIST
} from '../utils/constants';
import { get } from '../utils/api';
export const setAlbums = (albums) => ({
  type: SET_ALBUMS,
  albums
});
export const addAlbums = (albums) => ({
  type: ADD_ALBUMS,
  albums
});
export const setArtists = (artists) => ({
  type: SET_ARTISTS,
  artists
});
export const addArtists = (artists) => ({
  type: ADD_ARTISTS,
  artists
});
export const setPlayList = (playlists) => ({
  type: SET_PLAYLIST,
  playlists
});
export const addPlaylist = (playlists) => ({
  type: ADD_PLAYLIST,
  playlists
});
export const initiateGetResult = (searchTerm) => {
  return async (dispatch) => {
    try {
      const API_URL = `https://api.spotify.com/v1/search?query=${encodeURIComponent(
        searchTerm
      )}&type=album,playlist,artist`;
      const result = await get(API_URL);
      console.log(result);
      const { albums, artists, playlists } = result;
      dispatch(setAlbums(albums));
      dispatch(setArtists(artists));
      return dispatch(setPlayList(playlists));
    } catch (error) {
      console.log('error', error);
    }
  };
};


다음 내용으로 utils 폴더에 새 파일 api.js를 만듭니다.


import axios from 'axios';
import { setAuthHeader } from './functions';

export const get = async (url, params) => {
  setAuthHeader();
  const result = await axios.get(url, params);
  return result.data;
};

export const post = async (url, params) => {
  setAuthHeader();
  const result = await axios.post(url, params);
  return result.data;
};


이 파일에서는 axios를 사용하여 API 호출을 수행하지만 그 전에는 setAuthHeader 함수를 호출하여 Authorization Header에 access_token을 추가합니다.


다음 내용으로 구성 요소 폴더 내에 새 파일 Loader.js를 만듭니다.


import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
const Loader = (props) => {
  const [node] = useState(document.createElement('div'));
  const loader = document.querySelector('#loader');

  useEffect(() => {
    loader.appendChild(node).classList.add('message');
  }, [loader, node]);

  useEffect(() => {
    if (props.show) {
      loader.classList.remove('hide');
      document.body.classList.add('loader-open');
    } else {
      loader.classList.add('hide');
      document.body.classList.remove('loader-open');
    }
  }, [loader, props.show]);

  return ReactDOM.createPortal(props.children, node);
};
export default Loader;


이 파일에서는 백그라운드 오버레이와 함께 로딩 메시지를 표시 할 로더 구성 요소를 만들었습니다. 우리는 ReactDOM.createPortal 메소드를 사용하여 로더를 생성했습니다.


페이지에 로더를 추가하려면 public / index.html 파일을 열고 id root가있는 div 뒤에 로더 div를 추가하십시오.


index.html 페이지 본문은 이제 다음과 같습니다.


<body>
  <noscript>You need to enable JavaScript to run this app.</noscript>
  <div id="root"></div>
  <div id="loader" class="hide"></div>
  <!--
    This HTML file is a template.
    If you open it directly in the browser, you will see an empty page.
    You can add webfonts, meta tags, or analytics to this file.
    The build step will place the bundled scripts into the <body> tag.
    To begin the development, run `npm start` or `yarn start`.
    To create a production bundle, use `npm run build` or `yarn build`.
  -->
</body>


기본적으로 로더는 숨겨져 있으므로 숨김 클래스를 추가했으며 로더를 표시하는 동안 숨김 클래스를 제거합니다.


다음 내용으로 구성 요소 폴더 내에 새 파일 SearchForm.js를 만듭니다.


import React, { useState } from 'react';
import { Form, Button } from 'react-bootstrap';
const SearchForm = (props) => {
  const [searchTerm, setSearchTerm] = useState('');
  const [errorMsg, setErrorMsg] = useState('');
  const handleInputChange = (event) => {
    const searchTerm = event.target.value;
    setSearchTerm(searchTerm);
  };
  const handleSearch = (event) => {
    event.preventDefault();
    if (searchTerm.trim() !== '') {
      setErrorMsg('');
      props.handleSearch(searchTerm);
    } else {
      setErrorMsg('Please enter a search term.');
    }
  };
  return (
    <div>
      <Form onSubmit={handleSearch}>
        {errorMsg && <p className="errorMsg">{errorMsg}</p>}
        <Form.Group controlId="formBasicEmail">
          <Form.Label>Enter search term</Form.Label>
          <Form.Control
            type="search"
            name="searchTerm"
            value={searchTerm}
            placeholder="Search for album, artist or playlist"
            onChange={handleInputChange}
            autoComplete="off"
          />
        </Form.Group>
        <Button variant="info" type="submit">
          Search
        </Button>
      </Form>
    </div>
  );
};
export default SearchForm;


이 파일에서 검색 상자를 추가했으며 입력 값을 기반으로 구성 요소의 상태를 업데이트합니다.


다음 내용으로 구성 요소 폴더 내에 새 파일 SearchResult.js를 만듭니다.


import React from 'react';
import _ from 'lodash';
import AlbumsList from './AlbumsList';
const SearchResult = (props) => {
  const { result, setCategory, selectedCategory } = props;
  const { albums, artists, playlist } = result;
  return (
    <React.Fragment>
      <div className="search-buttons">
        {!_.isEmpty(albums.items) && (
          <button
            className={`${
              selectedCategory === 'albums' ? 'btn active' : 'btn'
            }`}
            onClick={() => setCategory('albums')}
          >
            Albums
          </button>
        )}
        {!_.isEmpty(artists.items) && (
          <button
            className={`${
              selectedCategory === 'artists' ? 'btn active' : 'btn'
            }`}
            onClick={() => setCategory('artists')}
          >
            Artists
          </button>
        )}
        {!_.isEmpty(playlist.items) && (
          <button
            className={`${
              selectedCategory === 'playlist' ? 'btn active' : 'btn'
            }`}
            onClick={() => setCategory('playlist')}
          >
            PlayLists
          </button>
        )}
      </div>
      <div className={`${selectedCategory === 'albums' ? '' : 'hide'}`}>
        {albums && <AlbumsList albums={albums} />}
      </div>
    </React.Fragment>
  );
};
export default SearchResult;


내부 이미지 폴더는 여기에서 이름이 music.jpeg 인 이미지를 추가합니다.


앨범, 아티스트 또는 재생 목록의 이미지가 없는 경우 이 이미지를 기본 이미지로 사용합니다.


다음 내용으로 구성 요소 폴더 내에 새 파일 AlbumsList.js를 만듭니다.


import React from 'react';
import { Card } from 'react-bootstrap';
import _ from 'lodash';
import music from '../images/music.jpeg';
const AlbumsList = ({ albums }) => {
  return (
    <React.Fragment>
      {Object.keys(albums).length > 0 && (
        <div className="albums">
          {albums.items.map((album, index) => {
            return (
              <React.Fragment key={index}>
                <Card style={{ width: '18rem' }}>
                  <a
                    target="_blank"
                    href={album.external_urls.spotify}
                    rel="noopener noreferrer"
                    className="card-image-link"
                  >
                    {!_.isEmpty(album.images) ? (
                      <Card.Img
                        variant="top"
                        src={album.images[0].url}
                        alt=""
                      />
                    ) : (
                      <img src={music} alt="" />
                    )}
                  </a>
                  <Card.Body>
                    <Card.Title>{album.name}</Card.Title>
                    <Card.Text>
                      <small>
                        {album.artists.map((artist) => artist.name).join(', ')}
                      </small>
                    </Card.Text>
                  </Card.Body>
                </Card>
              </React.Fragment>
            );
          })}
        </div>
      )}
    </React.Fragment>
  );
};
export default AlbumsList;


이제 yarn start 명령을 실행하여 앱을 시작하십시오.


API Response 


보시다시피 무엇이든 검색하면 Spotify API의 응답이 콘솔에 표시됩니다. 따라서 Spotify의 음악 데이터에 성공적으로 액세스 할 수 있습니다.


UI에 앨범 표시 


이제 UI에 표시 할 수 있도록 redux 저장소에 응답을 추가합니다.


src / reducers / albums.js 파일을 열고 다음 내용으로 바꿉니다.


import { SET_ALBUMS, ADD_ALBUMS } from '../utils/constants';
const albumsReducer = (state = {}, action) => {
  const { albums } = action;
  switch (action.type) {
    case SET_ALBUMS:
      return albums;
    case ADD_ALBUMS:
      return {
        ...state,
        next: albums.next,
        items: [...state.items, ...albums.items]
      };
    default:
      return state;
  }
};
export default albumsReducer;


이제 yarn start 명령을 다시 실행하고 응용 프로그램을 확인하십시오.


Albums 


보시다시피 검색하면 redux 저장소가 업데이트 되고 결과가 UI에 표시됩니다. 이 기능에 대한 코드를 이해하겠습니다.


Dashboard.js 파일에서 사용자가 검색 버튼을 클릭하면 트리거 되는 handleSearch 함수 내에서 initializeGetResult를 호출합니다.


actions / result.js 파일에서 initializeGetResult 함수를 확인하면 검색 텍스트를 쿼리 매개 변수로 전달하여 https://api.spotify.com/v1/search URL에 대한 API 호출을 수행합니다.


export const initiateGetResult = (searchTerm) => {
  return async (dispatch) => {
    try {
      const API_URL = `https://api.spotify.com/v1/search?query=${encodeURIComponent(
        searchTerm
      )}&type=album,playlist,artist`;
      const result = await get(API_URL);
      console.log(result);
      const { albums, artists, playlists } = result;
      dispatch(setAlbums(albums));
      dispatch(setArtists(artists));
      return dispatch(setPlayList(playlists));
    } catch (error) {
      console.log('error', error);
    }
  };
};


결과를 얻으면 결과에서 앨범을 가져 와서 setAlbums 액션 생성기 함수를 호출합니다.


dispatch(setAlbums(albums));


setAlbums 함수는 다음과 같습니다.


export const setAlbums = (albums) => ({
  type: SET_ALBUMS,
  albums
});


여기서는 SET_ALBUMS 유형의 작업을 반환합니다. 따라서 액션이 전달되면 reducers / albums.js 파일의 albumsReducer가 호출됩니다. 여기서 일치하는 SET_ALBUMS 스위치 케이스의 경우 감속기에서 전달 된 앨범을 반환하므로 redux 저장소가 앨범 데이터로 업데이트 됩니다.


case SET_ALBUMS:
      return albums;


connect 메서드를 사용하여 Dashboard 구성 요소 (Dashboard.js)를 redux 저장소에 연결 했으므로 구성 요소는 mapStateToProps 메서드를 사용하여 업데이트 된 redux 저장소 데이터를 가져오고 그 결과를 SearchResult 구성 요소에 전달합니다.


const { albums, artists, playlist } = props;
const result = { albums, artists, playlist };
<SearchResult
  result={result}
  setCategory={setCategory}
  selectedCategory={selectedCategory}
/>


SearchResult 구성 요소에서 데이터는 AlbumsList 구성 요소에 소품으로 전달됩니다.


<div className={`${selectedCategory === 'albums' ? '' : 'hide'}`}>
  {albums && <AlbumsList albums={albums} />}
</div>


AlbumsList 구성 요소 내에서 Array map 메서드를 사용하여 각 앨범을 반복하고 UI에 데이터를 표시합니다.


UI에 아티스트 및 재생 목록 표시 


다음 내용으로 구성 요소 폴더 내에 새 파일 ArtistsList.js를 만듭니다.


import React from 'react';
import { Card } from 'react-bootstrap';
import _ from 'lodash';
import music from '../images/music.jpeg';
const ArtistsList = ({ artists }) => {
  return (
    <React.Fragment>
      {Object.keys(artists).length > 0 && (
        <div className="artists">
          {artists.items.map((artist, index) => {
            return (
              <React.Fragment key={index}>
                <Card style={{ width: '18rem' }}>
                  <a
                    target="_blank"
                    href={artist.external_urls.spotify}
                    rel="noopener noreferrer"
                    className="card-image-link"
                  >
                    {!_.isEmpty(artist.images) ? (
                      <Card.Img
                        variant="top"
                        src={artist.images[0].url}
                        alt=""
                      />
                    ) : (
                      <img src={music} alt="" />
                    )}
                  </a>
                  <Card.Body>
                    <Card.Title>{artist.name}</Card.Title>
                  </Card.Body>
                </Card>
              </React.Fragment>
            );
          })}
        </div>
      )}
    </React.Fragment>
  );
};
export default ArtistsList;


다음 내용으로 구성 요소 폴더 내에 새 파일 PlayList.js를 만듭니다.


import React from 'react';
import { Card } from 'react-bootstrap';
import _ from 'lodash';
import music from '../images/music.jpeg';
const PlayList = ({ playlist }) => {
  return (
    <div>
      {Object.keys(playlist).length > 0 && (
        <div className="playlist">
          {playlist.items.map((item, index) => {
            return (
              <React.Fragment key={index}>
                <Card style={{ width: '18rem' }}>
                  <a
                    target="_blank"
                    href={item.external_urls.spotify}
                    rel="noopener noreferrer"
                    className="card-image-link"
                  >
                    {!_.isEmpty(item.images) ? (
                      <Card.Img variant="top" src={item.images[0].url} alt="" />
                    ) : (
                      <img src={music} alt="" />
                    )}
                  </a>
                  <Card.Body>
                    <Card.Title>{item.name}</Card.Title>
                    <Card.Text>
                      <small>By {item.owner.display_name}</small>
                    </Card.Text>
                  </Card.Body>
                </Card>
              </React.Fragment>
            );
          })}
        </div>
      )}
    </div>
  );
};
export default PlayList;


이제 SearchResult.js 파일을 열고 AlbumsList와 함께 ArtistsList 및 PlayList 구성 요소를 추가하십시오.


<div className={`${selectedCategory === 'albums' ? '' : 'hide'}`}>
  {albums && <AlbumsList albums={albums} />}
</div>
<div className={`${selectedCategory === 'artists' ? '' : 'hide'}`}>
  {artists && <ArtistsList artists={artists} />}
</div>
<div className={`${selectedCategory === 'playlist' ? '' : 'hide'}`}>
  {playlist && <PlayList playlist={playlist} />}
</div>


또한 파일 상단의 구성 요소를 가져옵니다.

import ArtistsList from './ArtistsList';
import PlayList from './PlayList';



src / reducers / artists.js 파일을 열고 다음 내용으로 바꿉니다.


import { SET_ARTISTS, ADD_ARTISTS } from '../utils/constants';
const artistsReducer = (state = {}, action) => {
  const { artists } = action;
  switch (action.type) {
    case SET_ARTISTS:
      return artists;
    case ADD_ARTISTS:
      return {
        ...state,
        next: artists.next,
        items: [...state.items, ...artists.items]
      };
    default:
      return state;
  }
};
export default artistsReducer;


src / reducers / playlist.js 파일을 열고 다음 내용으로 바꿉니다.


import { SET_PLAYLIST, ADD_PLAYLIST } from '../utils/constants';
const playlistReducer = (state = {}, action) => {
  const { playlists } = action;
  switch (action.type) {
    case SET_PLAYLIST:
      return playlists;
    case ADD_PLAYLIST:
      return {
        ...state,
        next: playlists.next,
        items: [...state.items, ...playlists.items]
      };
    default:
      return state;
  }
};
export default playlistReducer;


이제 yarn start 명령을 다시 실행하고 응용 프로그램을 확인하십시오.


Populated data 



보시다시피 아티스트와 재생 목록도 데이터로 채워집니다.


Play Music 


또한 이미지를 클릭하면 위와 같이 앨범, 아티스트, 플레이리스트의 음악을 재생할 수 있습니다.


로드 더 많은 기능 추가 


이제 앨범, 아티스트 및 재생 목록에 대한 더 많은 데이터를 로드하기 위해 더로드 버튼을 추가하겠습니다.


SearchResult.js 파일을 열고 종료 </React.Fragment> 태그 바로 앞에 추가 로드 버튼을 추가합니다.


{!_.isEmpty(result[selectedCategory]) &&
 !_.isEmpty(result[selectedCategory].next) && (
  <div className="load-more" onClick={() => loadMore(selectedCategory)}>
    <Button variant="info" type="button">
      Load More
    </Button>
  </div>
)}


props에서 loadMore 함수를 분해하고 react-bootstrap에서 Button을 가져옵니다.


import { Button } from 'react-bootstrap';
const SearchResult = (props) => {
const { loadMore, result, setCategory, selectedCategory } = props;



Dashboard.js 파일을 열고 loadMore 함수를 추가합니다.


const loadMore = async (type) => {
  const { dispatch, albums, artists, playlist } = props;
  setIsLoading(true);
  switch (type) {
    case 'albums':
      await dispatch(initiateLoadMoreAlbums(albums.next));
      break;
    case 'artists':
      await dispatch(initiateLoadMoreArtists(artists.next));
      break;
    case 'playlist':
      await dispatch(initiateLoadMorePlaylist(playlist.next));
      break;
    default:
  }
  setIsLoading(false);
};


loadMore 함수를 소품으로 SearchResult 구성 요소에 전달합니다.


return (
  <React.Fragment>
    <Header />
    <SearchForm handleSearch={handleSearch} />
    <Loader show={isLoading}>Loading...</Loader>
    <SearchResult
      result={result}
      loadMore={loadMore}
      setCategory={setCategory}
      selectedCategory={selectedCategory}
    />
  </React.Fragment>
);


actions / result.js 파일을 열고 파일 끝에 다음 함수를 추가합니다.


export const initiateLoadMoreAlbums = (url) => {
  return async (dispatch) => {
    try {
      console.log('url', url);
      const result = await get(url);
      console.log('categoriess', result);
      return dispatch(addAlbums(result.albums));
    } catch (error) {
      console.log('error', error);
    }
  };
};
export const initiateLoadMoreArtists = (url) => {
  return async (dispatch) => {
    try {
      console.log('url', url);
      const result = await get(url);
      console.log('categoriess', result);
      return dispatch(addArtists(result.artists));
    } catch (error) {
      console.log('error', error);
    }
  };
};
export const initiateLoadMorePlaylist = (url) => {
  return async (dispatch) => {
    try {
      console.log('url', url);
      const result = await get(url);
      console.log('categoriess', result);
      return dispatch(addPlaylist(result.playlists));
    } catch (error) {
      console.log('error', error);
    }
  };
};


상단의 Dashboard.js 파일 내에서 이러한 함수를 가져옵니다.


import {
  initiateGetResult,
  initiateLoadMoreAlbums,
  initiateLoadMorePlaylist,
  initiateLoadMoreArtists
} from '../actions/result';



이제 yarn start 명령을 실행하고 더 많은 기능을 로드하십시오.


Load More 


이 지점에서 이 지점까지 코드를 찾을 수 있습니다.


세션 시간 초과시 로그인 페이지로 리디렉션 


이제 앱의 기능을 마쳤습니다. 로그인 페이지로 자동 리디렉션되는 코드를 추가하고 액세스 토큰이 만료되면 세션이 만료되었다는 메시지를 표시합니다. 세션이 만료되면 API 호출이 실패하지만 사용자가 devtool 콘솔을 열어 오류를 볼 때까지 사용자는 이에 대해 알 수 없기 때문입니다.


RedirectPage.js 파일에서 다음 코드를 사용하여 로컬 저장소에 expiry_time을 추가했습니다.


const expiryTime = new Date().getTime() + access_token.expires_in * 1000;
localStorage.setItem('expiry_time', expiryTime);


이제 이것을 사용하여 로그인 페이지로 리디렉션 할시기를 식별 해 보겠습니다.


AppRouter.js 파일을 열고 다음 내용으로 바꿉니다.

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Home from '../components/Home';
import RedirectPage from '../components/RedirectPage';
import Dashboard from '../components/Dashboard';
import NotFoundPage from '../components/NotFoundPage';
class AppRouter extends React.Component {
  state = {
    expiryTime: '0'
  };
  componentDidMount() {
    let expiryTime;
    try {
      expiryTime = JSON.parse(localStorage.getItem('expiry_time'));
    } catch (error) {
      expiryTime = '0';
    }
    this.setState({ expiryTime });
  }
  setExpiryTime = (expiryTime) => {
    this.setState({ expiryTime });
  };
  isValidSession = () => {
    const currentTime = new Date().getTime();
    const expiryTime = this.state.expiryTime;
    const isSessionValid = currentTime < expiryTime;

    return isSessionValid;
  };
  render() {
    return (
      <BrowserRouter>
        <div className="main">
          <Switch>
            <Route path="/" component={Home} exact={true} />
            <Route path="/redirect" component={RedirectPage} />
            <Route path="/dashboard" component={Dashboard} />
            <Route component={NotFoundPage} />
          </Switch>
        </div>
      </BrowserRouter>
    );
  }
}
export default AppRouter;



이 파일에서는 기본적으로 0으로 초기화 된 상태 변수 expiryTime을 추가했으며 componentDidMount 메서드에서는 로컬 저장소에서 expiry_time 값을 읽어 상태에 할당합니다.


또한 다른 구성 요소에서 사용할 수 있도록 setExpiryTime 및 isValidSession 함수를 추가했습니다.


이제 RedirectPage.js 파일을 열고 history.push ( '/ dashboard'); 다음 코드 줄을 추가하십시오.


setExpiryTime(expiryTime);


그러나 이 함수를 호출하려면 RedirectPage 컴포넌트에 prop으로 전달해야 합니다.


AppRouter 컴포넌트의 렌더링 메소드를 확인하면 다음과 같습니다.


render() {
  return (
    <BrowserRouter>
      <div className="main">
        <Switch>
          <Route path="/" component={Home} exact={true} />
          <Route path="/redirect" component={RedirectPage} />
          <Route path="/dashboard" component={Dashboard} />
          <Route component={NotFoundPage} />
        </Switch>
      </div>
    </BrowserRouter>
  );
}



따라서 setExpiryTime 함수를 소품으로 RedirectPage 구성 요소에 전달하려면이를 렌더링 소품 패턴으로 변환해야 합니다.


따라서 아래 코드 줄을 변경하십시오.


<Route path="/redirect" component={RedirectPage} />


이 코드에 :


<Route
  path="/redirect"
  render={(props) => (
    <RedirectPage
      isValidSession={this.isValidSession}
      setExpiryTime={this.setExpiryTime}
      {...props}
    />
  )}
/>


여기서는 setExpiryTime, isValidSession 함수를 prop으로 전달하고 위치, 히스토리와 같이 Route에 자동으로 전달되는 props도 분산시킵니다.


이제 Dashboard.js 파일을 열고 소품을 구조화하고 handleSearch 함수를 다음과 같이 변경하십시오.


const { isValidSession, history } = props;
const handleSearch = (searchTerm) => {
  if (isValidSession()) {
    setIsLoading(true);
    props.dispatch(initiateGetResult(searchTerm)).then(() => {
      setIsLoading(false);
      setSelectedCategory('albums');
    });
  } else {
    history.push({
      pathname: '/',
      state: {
        session_expired: true
      }
    });
  }
};


또한 loadMore 함수를 다음과 같이 변경하십시오.

const loadMore = async (type) => {
  if (isValidSession()) {
    const { dispatch, albums, artists, playlist } = props;
    setIsLoading(true);
    switch (type) {
      case 'albums':
        await dispatch(initiateLoadMoreAlbums(albums.next));
        break;
      case 'artists':
        await dispatch(initiateLoadMoreArtists(artists.next));
        break;
      case 'playlist':
        await dispatch(initiateLoadMorePlaylist(playlist.next));
        break;
      default:
    }
    setIsLoading(false);
  } else {
    history.push({
      pathname: '/',
      state: {
        session_expired: true
      }
    });
  }
};



반환 된 JSX를 Dashboard 구성 요소에서 다음과 같이 변경합니다.


return (
  <React.Fragment>
    {isValidSession() ? (
      <div>
        <Header />
        <SearchForm handleSearch={handleSearch} />
        <Loader show={isLoading}>Loading...</Loader>
        <SearchResult
          result={result}
          loadMore={loadMore}
          setCategory={setCategory}
          selectedCategory={selectedCategory}
          isValidSession={isValidSession}
        />
      </div>
    ) : (
      <Redirect
        to={{
          pathname: '/',
          state: {
            session_expired: true
          }
        }}
      />
    )}
  </React.Fragment>
);


또한 상단에서 리디렉션 구성 요소를 가져옵니다.


import { Redirect } from 'react-router-dom';


SearchResult.js 파일을 열고 JSX를 반환하기 전에 다음 코드를 추가합니다.


if (!isValidSession()) {
  return (
    <Redirect
      to={{
        pathname: '/',
        state: {
          session_expired: true
        }
      }}
    />
  );
}


또한 props에서 isValidSession을 분해하고 react-router-dom에서 Redirect 구성 요소를 추가합니다.


이제 Home.js 파일을 열고 다음 내용으로 바꿉니다.


import React from 'react';
import { Alert } from 'react-bootstrap';
import { connect } from 'react-redux';
import { Button } from 'react-bootstrap';
import Header from './Header';
import { Redirect } from 'react-router-dom';
const Home = (props) => {
  const {
    REACT_APP_CLIENT_ID,
    REACT_APP_AUTHORIZE_URL,
    REACT_APP_REDIRECT_URL
  } = process.env;
  const handleLogin = () => {
    window.location = `${REACT_APP_AUTHORIZE_URL}?client_id=${REACT_APP_CLIENT_ID}&redirect_uri=${REACT_APP_REDIRECT_URL}&response_type=token&show_dialog=true`;
  };
  const { isValidSession, location } = props;
  const { state } = location;
  const sessionExpired = state && state.session_expired;

  return (
    <React.Fragment>
      {isValidSession() ? (
        <Redirect to="/dashboard" />
      ) : (
        <div className="login">
          <Header />
          {sessionExpired && (
            <Alert variant="info">Session expired. Please login again.</Alert>
          )}
          <Button variant="info" type="submit" onClick={handleLogin}>
            Login to spotify
          </Button>
        </div>
      )}
    </React.Fragment>
  );
};
export default connect()(Home);


여기에는 세션이 유효하면 / dashboard 페이지로 리디렉션하는 코드가 있고 그렇지 않으면 로그인 페이지로 리디렉션됩니다. 또한 세션 만료 메시지를 표시하여 사용자가 페이지가 로그인 페이지로 리디렉션되는 이유를 알 수 있습니다.


{sessionExpired && (
  <Alert variant="info">Session expired. Please login again.</Alert>
)}


제 AppRouter.js 파일을 열고 isValidSession 함수를 홈 및 대시 보드 경로에 전달하십시오.


render() {
  return (
    <BrowserRouter>
      <div className="main">
        <Switch>
          <Route
            path="/"
            exact={true}
            render={(props) => (
              <Home isValidSession={this.isValidSession} {...props} />
            )}
          />
          <Route
            path="/redirect"
            render={(props) => (
              <RedirectPage
                isValidSession={this.isValidSession}
                setExpiryTime={this.setExpiryTime}
                {...props}
              />
            )}
          />
          <Route
            path="/dashboard"
            render={(props) => (
              <Dashboard isValidSession={this.isValidSession} {...props} />
            )}
          />
          <Route component={NotFoundPage} />
        </Switch>
      </div>
    </BrowserRouter>
  );
}


세션 시간이 초과되면 다음 화면이 표시됩니다.


Session expired 


이 지점에서 이 지점까지 코드를 찾을 수 있습니다.


결론 


이제 React를 사용하여 Spotify 음악 검색 앱 생성을 마쳤습니다. 여기에서 이 애플리케이션의 전체 소스 코드를 찾을 수 있습니다.