Spotify API 사용 (Node.js, express, React)

Bumgu

2024/01/31

Tags: React express node.js Spotify

깃허브 링크

원래 Next.js로 구현해보려 했으나 OAuth2.0기반 로그인도 처음 구현해보는데, Next.js의 숙련도도 부족해 Node.js + Express + React로 구현했습니다.

1. Spotify의 로그인 방식

Spotify 는 OAuth2.0 프레임워크를 사용합니다.

먼저 API를 사용하기위해 필요한건

흐름은 1.client_secret, client_id (스포티파이 개발자 대쉬보드에서 발급) 2.code발급 3.access_token,refresh_token 발급 4.access_token을 이용해 API사용 가능 5.access_token만료 시 refresh_token을 이용해 다시 발급 이후 다시 4번 > 5번 > 4번 > 5번 … 이 됩니다.

2. 스포티파이 개발자 대쉬보드

https://developer.spotify.com 로 접속하고 로그인합니다. 이후 오른쪽 위 이름을 클릭해 대쉬보드로 이동하고 create app버튼을 클릭합니다.

App name : 앱의 이름 App description : 앱의 설명 (필수 x) Website : 웹사이트의 주소 (필수 x) Redirect URI : 필수! 코드 발급 시 리다이렉트될 콜백 페이지입니다. 보통 개발환경에서는 http://localhost:3000/callback 등으로 설정합니다.

그리고 본인이 사용 할 API를 클릭합니다. 이후에 수정이 가능합니다. 이후 대쉬보드에서 만든 앱을 클릭하고 setting에 들어가면 clientId, client secret 확인이 가능합니다.

3. 프로젝트 세팅

3-1. Express서버

리액트 앱을 만들고 그 안에 Express서버를 세팅하겠습니다. yarn create react-app appname 이후 react 폴더에 yarn add express 명령어로 express를 설치하고 server라는 폴더를 만들고 index.js에 express서버를 만듭니다.

const express = require('express');
const path = require('path');
const app = express();

app.use(express.static(path.join(__dirname, '..', 'build')));
app.use(express.json());


app.get('/', function (req, res) {
  res.sendFile(path.join(__dirname, '..', 'build', 'index.html'));
});

app.listen(9000,() => {
  console.log('SERVER LISTENING ON 9000')
}) ;
app.get('/', function (req, res) {
  res.sendFile(path.join(__dirname, '..', 'build', 'index.html'));
});

build폴더의 index.html을 렌더링합니다. 아직 build폴더가 없기 때문에 yarn build명령어를 통해 빌드해줍니다.

Express는 9000, 리액트는 3000에서 실행됩니다. 하지만 저는 하나의 포트에서 관리하고 싶으므로 React Proxy세팅을 해주도록 하겠습니다.

3-2. React Proxy

공식문서 src/경로에 setUpProxy.js를 만듭니다.

const { createProxyMiddleware } = require('http-proxy-middleware');


module.exports = function(app) {
  app.use(
    '/api',
    createProxyMiddleware({
      target: 'http://localhost:9000',
      changeOrigin: true,
    })
  )
}

저는 백엔드 엔드포인트를 /api경로에 둘 것이기 때문에 /api로 했고, target은 Express 실행 포트인localhost:9000 로 했습니다. 프록시 설정을 하고나면 리액트 실행포트인 3000포트로 (localhost:3000/api/~~~)으로 요청해도 Express서버의 실행포트은 9000포트로 바꿔줍니다. 즉 하나의 포트만 사용하듯이 사용할 수 있습니다.

4. Spotify Token 요청

4-1. code 발급

우선 코드발급의 흐름은 https://acounts.spotify.com/authorize경로에 쿼리스트링으로 데이터들을 넣어서 요청하면 대쉬보드에서 설정한 Redirect_uri(콜백페이지)로 리디렉션되며 리디렉션된 콜백페이지의 쿼리스트링으로 담겨옵니다.

URLmehtod필요한 데이터
https://accounts.spotify.com/authorizeGETresponse_type, client_id, scope, redirect_uri

https://accounts.spotify.com/authorize?response_type=code&client_id=$CLINET_ID&scope=${SCOPE}&redirect_uri=${REDIRECT_URI} 의 모습을 띕니다.

response_type : code를 발급받기에 code를 입력합니다. client_id : 스포티파이 대쉬보드에서 부여받은 번호 scope : scope에 따라 접근할 수 있는 데이터의 범위가 달라집니다 scope list redirect_uri : 스포티파이 대쉬보드에서 설정한 콜백페이지 주소

이제 코드로 써보겠습니다.

const express = require('express');
const { SPOTIFY_CLIENT_ID, SCOPE, REDIRECT_URI } = require('../config');

const apiRouter = express.Router();


apiRouter.get('/login', (req, res) => {
  res.redirect(`https://accounts.spotify.com/authorize?response_type=code&client_id=${SPOTIFY_CLIENT_ID}&scope=${SCOPE}&redirect_uri=${REDIRECT_URI}`);
});

/api경로로 들어오는 요청을 수행하는 파일이기 때문에 라우터 설정을 했습니다. 필요한 환경변수들은 dotenv를 사용해 config.js파일에 설정했고, import 해왔습니다. 이 경로로 필요한데이터를 입력하고 요청을하게되면 설정한 redirect_uri로 리디렉션되며 주소창을 보면 /callback?code=.....의 형식을 띄고있는데 여기서 code를 가져오면 됩니다.

http://localhost:3000/api/callback?code=AQAYxRYFF7hAkwLNG-Q3qUJ9K7Wew3Dk6NKr-6bwk0POYHiSQg2QQLpukCOM-uA2YCkysqv1I... 의 형식을 가지고 있습니다.

4-2. Token 발급

이제 발급받은 code로 토큰을 발급해야 합니다.

본인이 스포티파이 대쉬보드에 설정했던 redirect_uri로 리디렉션이 되므로 그에 맞는 엔드포인트를 만듭니다. 저의 경우에는 api/callback였습니다.

apiRouter.get('/callback', (req, res) => {
  const searchParams = new URLSearchParams(req.query);
  const code = searchParams.get('code');

  const tokenResponse = await getSpotifyRefreshToken(code);// 스포티파이 토큰을 받아오는 함수를 tokenResponse에 저장

  if (tokenResponse) { // tokenResponse에 값이 있을 경우 쿠키에 저장
    res.cookie('refresh_token', tokenResponse.refresh_token, {
      httpOnly: true,
    })

  }
  res.redirect('http://localhost:3000'); // 이후 메인페이지로 리디렉션
});

우선 주소창의 code 값을 가져오고, 토큰과 교환하는 함수의 인자에 넣어줍니다

URLmethodheader필요한 데이터
https://accounts.spotify.com/api/tokenPOSTContent-Type: application/x-www-url-encoded, Authorization: Basic ${basicToken}grant_type, redirect_uri, code
async function getSpotifyRefreshToken(code) {
  const searchParams = new URLSearchParams();
  searchParams.append('grant_type', 'authorization_code');
  searchParams.append('redirect_uri', REDIRECT_URI);
  searchParams.append('code', code);

 const basicToken = new Buffer.from(SPOTIFY_CLIENT_ID + ':' + SPOTIFY_CLIENT_SECRET).toString('base64');

  try {
   const response = await axios.post(SPOTIFY_TOKEN_ENDPOINT, searchParams, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Basic ${basicToken}`,
      },
    });

    return response.data;
  } catch (err) {
    console.error(err);
  }
  return null;
}

필요한 데이터들을 URLSearchParams객체에 넣어줍니다. basicToken은 base64 방식으로 인코딩된 client_id : client_secret입니다. axios를 이용해 post방식으로 토큰을 받는 스포티파이 엔드포인트https://accounts.spotify.com/api/token 에 데이터를 넣어 요청합니다. 이후 JSON데이터로 반환이되고 반환 값은

{
"access_token":"BQD-8jTKGyYS7W-LfaMvwj_vgGxRoFiWtVKfwZPovf15_tjYbOXGF9sg_p9KS1VRNIqeox2TpiiFAhncntC...."
,"token_type":"Bearer"
,"expires_in":3600,
"refresh_token":"AQCIF3IBVLc_R560C-sB_YtOfophiZjrw...",
"scope":"user-top-read"
}

의 형식을 가지고 있습니다. 다시 api/callback 의 코드를 보면

const tokenResponse = await getSpotifyRefreshToken(code);
if (tokenResponse) { // tokenResponse에 값이 있을 경우 쿠키에 저장
    res.cookie('refresh_token', tokenResponse.refresh_token, {
      httpOnly: true,
    })

반환받은 response의 refresh_token값을 쿠키에 넣어줍니다. 메인페이지 접속시 쿠키의 refresh_token의 값을 확인하고, 있다면 데이터를 렌더링하고, 없다면 로그인 버튼을 렌더링하기 위해 쿠키에 저장했습니다.

이제

/api/login으로 요청 > 스포티파이 로그인창 > /api/callback(토큰발급 후 쿠키에 저장) > 메인페이지로 리디렉션

의 흐름이 완성되었습니다. /api/login으로 접속하고 스포티파이 로그인을 하고 개발자탭에서 Application탭을 확인해보면

쿠키가 저장되는 모습을 확인할 수 있습니다.

이제 이 토큰을 가지고 실제 스포티파이 유저 데이터를 받아오고, 화면에 보여주면 됩니다.

5. API로 사용자 정보 요청

토큰까지 받아서 쿠키에 저장이 되었습니다. 제가 하려는건

  1. 메인페이지 접속 자동으로 쿠키의 토큰값을 검사
  1. 토큰이 있으면 사용자의 최근6개월간 많이 들은 트랙들을 렌더링
  2. 아닐경우 로그인 버튼을 렌더링

의 흐름입니다.

백엔드에서 세팅해준 refresh_tokenhttponly로 설정되어서 브라우저에서 값을 확인 할 수 없습니다. 그렇기때문에 쿠키의 값을 확인하고, 그 값으로 사용자 정보를 요청하는 백엔드 엔드포인트를 만들어주겠습니다.

5-1. 토큰확인 백엔드 엔드포인트

apiRouter.get('/token', async (req, res) => {
    const refreshToken = req.cookies.refresh_token; //쿠키에 저장된 토큰값

    const tokenResponse = await getSpotifyAccessTokenByRefreshToken(refreshToken);
    //토큰을 기반으로 액세스토큰 발급

    if(tokenResponse) {
    //액세스 토큰으로 유저프로필과 탑트랙데이터를 받아옴
      const topTrackData = await getTopTrackData(tokenResponse);
      const userData = await getUsersProfile(tokenResponse);
      res.status(200).json({topTrackData: topTrackData, userData: userData});
      // 데이터를 담은 JSON과함께 응답
    }
});

브라우저에서는 useEffect를 사용해 첫 렌더시 api/token으로 요청을 하고 쿠키의 토큰값에 따라 다음 행동이 결정됩니다.

이제 getSpotifyAccessTokenByRefreshToken() : 쿠키에 저장된 refresh_token의 값으로 access_token발급 getTopTrackData() : 발급받은 access_token으로 유저의 탑트랙 데이터 요청 getUsersProfile() : 발급받은 access_token으로 유저 프로필 데이터 요청

이 3개의 함수들을 작성하겠습니다.

5-2. 데이터 요청 함수

access_token 발급

async function getSpotifyAccessTokenByRefreshToken(refreshToken) {
  const searchParams = new URLSearchParams();
  searchParams.append('grant_type', 'refresh_token');
  searchParams.append('refresh_token', refreshToken);
  searchParams.append('client_id', SPOTIFY_CLIENT_ID);
  const basicToken = new Buffer.from(SPOTIFY_CLIENT_ID + ':' + SPOTIFY_CLIENT_SECRET).toString('base64');

  try {
    const response = await axios.post(SPOTIFY_TOKEN_ENDPOINT, searchParams, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Authorization': `Basic ${basicToken}`,
      },
    });

    return response.data.access_token;
  } catch (err) {
    console.error(err);
  }
  return null;
}
URLmethodheaders필요한 데이터
https://accounts.spotify.com/api/tokenPOSTContent-Type: application/x-www-form-urlencoded, Authorization : Basic ${basicToken}grant_type, refresh_token, client_id

grant_type : ‘refresh_token’ refresh_token : 쿠키에 저장된 refresh_token의 값 client_id 스포티파이 대쉬보드에서 부여받은 번호

URLSearchParams객체에 필요한데이터 들을 넣어주고, axios를 통해POST방식으로 요청합니다. 요청 후 응답 JSON객체에서 access_token을 반환합니다.

유저 탑 트랙 데이터 요청

async function getTopTrackData(accessToken) {
  try {
     const response = await axios.get(SPOTIFY_TOP_TRACK_ENDPOINT, {
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        })
    return response.data;
  } catch (err) {
    console.error(err);
  }
  return null;
}
URLmethodheaders필요한 데이터
https://api.spotify.com/v1/me/top/tracksGETAuthorization: Bearer ${access_token}-

유저의 최근 많이들은 트랙들을 받아오는 함수입니다. 이전의 Token발급과정과는 달리 단순합니다.

TopTrackData의 반환 JSON은 굉장히 많은 데이터를 포함하고 있습니다. 클라이언트에서 골라 사용할 것이기 때문에 response.data를 반환합니다.

유저 프로필 데이터 요청

async function getUsersProfile(accessToken) {
  try {
     const response = await axios.get(SPOTIFY_USER_DATA_ENDPOINT, {
            headers: {
                'Authorization': `Bearer ${accessToken}`
            }
        })
    return response.data;
  } catch (err) {
    console.error(err);
  }
  return null;
}
URLmethodheaders필요한 데이터
https://api.spotify.com/v1/me/GETAuthorization: Bearer ${access_token}-
유저의 탑트랙 데이터와 다른점은 URL밖에 없습니다. 이것 역시 JSON객체를 응답해주기 때문에 response.data를 반환합니다.

6. 프론트에서 데이터 받기

이제 백엔드에서의 작업은 끝났습니다. 프론트에서 보여주기만 하면 됩니다.

6-1. 데이터 패칭

  const [token, setToken] = useState(false);
  const [topTrackData, setTopTrackData] = useState([]);
  const [userData, setUserData] = useState('');
  useEffect(() => {
    async function fetchToken() {
      try{
        // cookie에 저장돼있는 토큰값을 확인하고 데이터 받아옴
        const response = await axios.get('/api/token');
        if (response) {
          setToken(true);
          setUserData(response.data.userData.display_name);
          setTopTrackData(response.data.topTrackData.items);
        }
      } catch (err) {
        console.error('Error fetching token :', err);
      }
    }
    fetchToken();
  }, [])

값을 세팅해줄 변수들은 만들었으니 이제 useEffect내부에 async function을 작성합니다.

6-2. 받아온 데이터 렌더링

이제 데이터 준비까지 끝났습니다. 이제 return문안에 데이터를 렌더링하겠습니다.

return (
  <div className="App">
    {token ? ( // token이 true일 때
      <>
        <h3>{userData}'s Top Track for last 6 months</h3>
        <div>
            <TrackList topTrackData={topTrackData} />
        </div>
      </>
    ) : ( // token이 false일 때
      <button onClick={() => window.location.href='/api/login'}>Login to Spotify</button>
    )}
  </div>
);

token값은 /api/token으로 요청후 응답이 있는 경우에 true가 됩니다. 즉 로그인 하기 전에는 false입니다. 그렇기 때문에 삼항연산자를 이용해 조건부렌더링을 해줍니다.

유저의 이름은 이미 response.data.display_name으로 할당했기에 바로 유저의 이름이 할당됩니다. 유저의 탑트랙 데이터는 설정한 조건에 따라 개수가 다르지만, 기본값은 최근 6개월간의 20개를 순서대로 보여줍니다. TrackList라는 컴포넌트를 만들어서 topTrackData를 props로 넘겨주겠습니다.

function TrackList({ topTrackData }) {
  return (
  <div>
      {topTrackData.map((track, index) => (
      <div key={index}>
          <h3>{track.album.name}</h3>
          <h3>{track.album.artists[0].name}</h3>
          <img src={track.album.images[1].url} alt={'artist image'}/>
        </div>
      ))}
    </div>
  )
}

export default TrackList;

스포티파이에서 받아온 topTrackData는 배열로 반환이 됩니다. .map함수를 사용해 차례대로 렌더링 해줍니다. 차례대로 앨범명, 가수이름, 앨범이미지 입니다. 가수이름은 가수가 1명인 경우가있고, 여러명인 경우가 있기에 artists라는 배열안에 값이 들어있습니다. 여기서 .map함수를 한번 더 사용해 모두 보여주도록 하겠습니다.

return (
  <div>
      {topTrackData.map((track, index) => (
      <div key={index}>
          <h3>{track.album.name}</h3>
          {track.album.artists.map((artist, index, arr) => (
          <span>{artist.name} {index < arr.length -1 && ' ,'}</span>
          ))}
          <div></div>
          <img src={track.album.images[1].url} alt={'artist image'}/>
        </div>
      ))}
    </div>
  )

track.album.artists는 위에서 말했듯이 배열입니다. .map함수를 사용해 artists배열을 순회하며 아티스트를 모두 보여주고, 여러명인 경우 {index < arr.length -1}를 통해 배열안의 마지막 아티스트가 아닌지를 확인하고 &&연산자를 통해 앞의 조건이 true일 경우 ,를 추가합니다. 줄바꿈을 위해 block요소인 빈<div>를 추가했습니다.

7. 결과 확인

이렇게 Spotify API를 사용해 유저의 최근 많이들은 노래들과, 유저 프로필의 이름을 가져와 보여줍니다. 포스팅은 여기서 마치겠습니다. 이후 계속 발전시킬 생각이라 따라했는데 잘 안되거나 코드가 궁금하신분은 깃허브 링크에서 확인 하시면 됩니다. 감사합니다.

>> Home