230619~21 - React(유튜브 API) 사용 + 리팩토링

백승연·2023년 6월 21일
1

🚩 React

router을 이용하여 youtube 사이트 구현하기 2~4일차

📝 설명

  • react 를 사용하여 유튜브 사이트 구현
  • 리팩토링 포함(리팩토링 전 코드는 주석처리)


✒️ 코드 작성

입력

App.js

import { Outlet } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
//서버 상태(가져온 데이터)를 관리하는 관리하는 툴
import SearchHeader from "./components/SearchHeader";
import "./App.css";
import { YoutubeApiProvider } from "./context/YoutubeApiContext";

const queryClient = new QueryClient();
//QueryClient클라스로 인스턴스를 만들어 줌

// 
function App() {
  return (
    <>
      <SearchHeader />
      <YoutubeApiProvider>
        <QueryClientProvider client={queryClient}>
          <Outlet />
        </QueryClientProvider>
      </YoutubeApiProvider>
    </>
  );
}

export default App;



index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import { createBrowserRouter, RouterProvider} from "react-router-dom";
import App from './App';
import NotFound from './pages/NotFound';
import Videos from './pages/Videos';
import VideoDetail from './pages/VideoDetail';
import './index.css';

const router = createBrowserRouter([
  {
    path:'/',
    element:<App />,
    errorElement:<NotFound />,
    children:[
      { index:true, element:<Videos />},
      { path:'/videos', element:<Videos />},
      { path:'/videos/watch/:videoId', element:<VideoDetail />},
      { path:'/videos/:keyword', element:<Videos />}
    ]
  },
  
])

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router} />
  </React.StrictMode>
);



  • pages 폴더

Videos.jsx

import React, { useContext } from "react";
import { useParams } from "react-router-dom";
import { useQuery } from "@tanstack/react-query";
import VideoCard from "../components/VideoCard";
import { useYoutubeApi } from "../context/YoutubeApiContext";
// import { search } from '../api/youtube';
// import FakeYoutube from "../api/fakeYoutube";
// import Youtube from "../api/youtube";

export default function Videos() {
  const { keyword } = useParams();
  const { youtube } = useYoutubeApi(); // context 파일에서 만들어 온 함수 실행

  const {
    isLoading,
    error,
    data: videos,
  } = useQuery(["videos", keyword], () => youtube.search(keyword));

  // const {
  //   isLoading,
  //   error,
  //   data: videos,
  // } = useQuery(["videos", keyword], () => {
  //   // context로 보냄
  //   // const youtube = new FakeYoutube(); // 외부 data를 가지고 오는 함수(생성자)
  //   // const youtube = new Youtube();
  //   return youtube.search(keyword);
  //   // 1. fakeYoutube.js의 함수에 keyword를 인자로 보내줌(검색한 내용)
  //   // 2. context에서 youtube 가져와야 함
  // });
  /*
    { 쿼리값, 에러 , 데이터 이름 }
    const { isLoading, error, data } = useQuery([],fnc,options)
    
    useQuery 
    - useState보다 효율적으로 관리하기 위해 사용(라이브러리)
    - 여기저기 가져다 사용할 수 있음
  */

  console.log("videos ? ", videos);
  return (
    <>
      <div>Videos - {keyword ? ` 🔍 ${keyword}` : "🔥인기동영상"} </div>
      {/* //keyword가 있을때 / 없을때  */}

      {isLoading && <p>Loading...</p>}
      {error && <p>🚨 에러발생 🚨</p>}

      {videos && (
        <ul>
          {videos.map((video) => (
            <VideoCard key={video.id} video={video} />
          ))}
        </ul>
      )}
    </>
  );
}



VideoDetail.jsx

import React from 'react'

export default function VideoDetail() {
  return (
    <div>VideoDetail</div>
  )
}



NotFound.jsx

import React from "react";

export default function NotFound() {
  return (
    <div>
      <h3>없는 페이지입니다.</h3>
    </div>
  );
}



  • components 폴더

SearchHeader.jsx

import React, { useState, useEffect } from 'react'
import { BsYoutube, BsSearch, BsPersonCircle, BsThreeDotsVertical } from "react-icons/bs";
import { Link,useNavigate,useParams } from "react-router-dom";

export default function SearchHeader() {
  const navigate = useNavigate();
  const { keyword } = useParams();
  const [text,setText] = useState('');
  
  const handleSubmit = (e) =>{
    e.preventDefault();
    navigate(`/videos/${text}`);    
  }

  //서치가 있을때만 인풋 텍스트가 보이게
  useEffect(()=>{
    setText(keyword || '')
  },[keyword])

  return (
    <div className="border-b border-zinc-500"> 
      <div className='w-full max-w-screen-2xl m-auto'>
        <header className='flex justify-between p-4'>
          <Link to='/' className='flex items-center'>
            <BsYoutube className='text-4xl text-brand'/>
            <h1 className='text-3xl font-LeagueGothic ml-3 tracking-wide'>Youtube</h1>
            <sup className='text-xs text-zinc-400 ml-2'>KR</sup>
          </Link>

          <form onSubmit={handleSubmit} className='flex justify-between border border-zinc-600 rounded-full pl-6 w-1/2 max-w-2xl '>
            <input 
              type="text" 
              placeholder='검색' 
              value={text}
              onChange={(e)=> setText(e.target.value)} 
              className='bg-zinc-900 text-zinc-400 outline-0 w-full' />
            <button className='border-l border-zinc-600 px-6 bg-zinc-700 rounded-r-full '  >
              <BsSearch />
            </button>
          </form>

          <div className='hidden sm:flex items-center'>
            <BsThreeDotsVertical className='text-lg' />
            <button className=' flex items-center border border-zinc-600 rounded-full p-2 text-sky-500 ml-4 hover:bg-sky-950'>
              <BsPersonCircle />
              <span className='text-sm pl-2'>로그인</span>
            </button>
            
          </div>
          

        </header>
      </div>
    </div>
  )
}



VideoCard.jsx

import React from "react";
import { formatAgo } from "../util/date";

export default function VideoCard({ video }) {
  const { thumbnails, title, channelTitle, publishedAt } = video.snippet;

  return (
    // *리팩토링
    <li>
      <img src={thumbnails.high.url} alt={title} />
      <div>
        <p>{title}</p>
        <p>{channelTitle}</p>
        {/* <p>{format(publishedAt, "ko")}</p> */}
        <p>{formatAgo(publishedAt, "ko")}</p>
      </div>
    </li>

    // <div>{video.snippet.title}</div>
  );
}



  • api 폴더

fakeYoutubeClient.js

// 리팩토링 전 파일명 : fakeYoutube.js

//내부 json로 연결 - 실제 유튜브 API를 가져올 수가 없음(사용제한)
// public 안의 videos 폴더에 json파일 존재
import axios from "axios";

// *리팩토링
// 여기서 class를 만들어서 보냄
export default class FakeYoutubeClient {
  // 키워드를 받아 올 필요가 없음
  async search() {
    return axios.get(`/videos/search.json`);
  }

  async videos() {
    return axios.get(`/videos/popular.json`);
  }
}

// export default class FakeYoutube {
//   constructor() {}

//   // 함수 3개 정의
//   // Videos.jsx에서 keyword를 받아옴
//   async search(keyword) {
//     return keyword ? this.#searchByKeyword(keyword) : this.#popular();
//   }

//   //아이디값만 수정해서 얻어옴
//   async #searchByKeyword(keyword) {
//     return axios
//       .get(`/videos/search.json`)
//       .then((res) => res.data.items)
//       .then((items) => items.map((item) => ({ ...item, id: item.id.videoId })));
//   }

//   async #popular() {
//     return axios.get(`/videos/popular.json`).then((res) => res.data.items);
//   }
// }

/*
export async function search(keyword) {
  return axios
    .get(`/videos/${keyword ? 'search' : 'popular'}.json`)
    .then((res)=>res.data.items)
*/



youtube.js

//실제 유튜브 연결 - 실제 유튜브 API
// https://developers.google.com/youtube/v3/getting-started

// import axios from "axios";

export default class Youtube {
  constructor(apiClient) {
    // 반복되는 부분 변수화
    // this.httpClient = axios.create({
    //   baseURL: "https://youtube.googleapis.com/youtube/v3",
    //   params: { key: process.env.REACT_APP_YOUTUBE_API_KEY },
    // });

    this.apiClient = apiClient; // 외부에서 받아옴
  }

  // * 리팩토링
  // 함수 3개 정의
  // Videos.jsx에서 keyword를 받아옴
  async search(keyword) {
    return keyword ? this.#searchByKeyword(keyword) : this.#popular();
  }

  async #searchByKeyword(keyword) {
    return this.apiClient
      .search({
        params: {
          part: "snippet",
          maxResults: 25,
          type: "video",
          q: keyword,
        },
      })
      .then((res) => res.data.items)
      .then((items) => items.map((item) => ({ ...item, id: item.id.videoId })));
  }

  async #popular() {
    return this.apiClient
      .videos({
        params: {
          part: "snippet",
          maxResults: 25,
          chart: "mostPopular",
        },
      })
      .then((res) => res.data.items);
  }
}

// 기존 youtubeClient.js가 없을 때 youtube.js 코드
// export default class Youtube {
//   constructor() {
//     // 반복되는 부분 변수화
//     // this.httpClient = axios.create({
//     //   baseURL: "https://youtube.googleapis.com/youtube/v3",
//     //   params: { key: process.env.REACT_APP_YOUTUBE_API_KEY },
//     // });

//     this.httpClient = httpClient // 외부에서 받아옴
//   }
//   // 함수 3개 정의
//   // Videos.jsx에서 keyword를 받아옴
//   async search(keyword) {
//     return keyword ? this.#searchByKeyword(keyword) : this.#popular();
//   }

//   //아이디값만 수정해서 얻어옴
//   async #searchByKeyword(keyword) {
//     return this.apiClient
//       .seach(`search`, {
//         params: {
//           part: "snippet",
//           maxResults: 25,
//           type: "video",
//           q: keyword, // 키워드로 전달받음
//         },
//       })
//       .then((res) => res.data.items);
//   }

//   async #popular() {
//     return this.httpClient
//       .get(`videos`, {
//         params: {
//           part: "snippet",
//           maxResults: 25,
//           chart: "mostPopular",
//           regionCode: "KR",
//         },
//       })
//       .then((res) => res.data.items);
//   }
// }

/*
export async function search(keyword) {
  return axios
    .get(`/videos/${keyword ? 'search' : 'popular'}.json`)
    .then((res)=>res.data.items)
*/



youtubeClient.js

// 리팩토링 전 파일명 : youtube.js (기존 youtube.js파일 복사하여 수정한 파일임)

//실제 유튜브 연결 - 실제 유튜브 API
// https://developers.google.com/youtube/v3/getting-started
import axios from "axios";

export default class YoutubeClient {
  constructor() {
    // 반복되는 부분 변수화
    this.httpClient = axios.create({
      baseURL: "https://youtube.googleapis.com/youtube/v3",
      params: { key: process.env.REACT_APP_YOUTUBE_API_KEY },
    });
  }

  // *리팩토링
  // 옵션만 처리
  async search(params) {
    return this.httpClient.get("search", params);
  }

  async videos(params) {
    return this.httpClient.get("videos", params);
  }

  // export default class Youtube {
  //   constructor() {
  //     // 반복되는 부분 변수화
  //     this.httpClient = axios.create({
  //       baseURL: "https://youtube.googleapis.com/youtube/v3",
  //       params: { key: process.env.REACT_APP_YOUTUBE_API_KEY },
  //     });
  //   }
  // //아이디값만 수정해서 얻어옴
  // async #searchByKeyword(keyword) {
  //   return this.httpClient
  //     .get(`search`, {
  //       params: {
  //         part: "snippet",
  //         maxResults: 25,
  //         type: "video",
  //         q: keyword, // 키워드로 전달받음
  //       },
  //     })
  //     .then((res) => res.data.items);
  // }

  // async #popular() {
  //   return this.httpClient
  //     .get(`videos`, {
  //       params: {
  //         part: "snippet",
  //         maxResults: 25,
  //         chart: "mostPopular",
  //         regionCode: "KR",
  //       },
  //     })
  //     .then((res) => res.data.items);
  // }
}

/*
export async function search(keyword) {
  return axios
    .get(`/videos/${keyword ? 'search' : 'popular'}.json`)
    .then((res)=>res.data.items)
*/



  • context 폴더

YoutubeApiContext.js

// context와 관련 된 파일을 모아 둔 js파일
import { createContext, useContext } from "react";
import FakeYoutubeClient from "../api/fakeYoutubeClient";
import Youtube from "../api/youtube";
import YoutubeClient from "../api/youtubeClient";
// import FakeYoutube from "../api/fakeYoutubeClient";

export const YoutubeApiContext = createContext();

// *리팩토링
const client = new FakeYoutubeClient();
// const client = new YoutubeClient();
const youtube = new Youtube(client);

// context가 적용될 영역을 지정하는 함수 정의
export function YoutubeApiProvider({ children }) {
  return (
    <YoutubeApiContext.Provider value={{youtube}}>
      {children}
    </YoutubeApiContext.Provider>
  );
}

// youtube value값에 접근할 수 있도록 하는 함수(Videos.jsx)
export function useYoutubeApi() {
  return useContext(YoutubeApiContext);
}

/*
  위와 같이 설정하면 youtube api 불러온 것을 모든 컴포넌트에서 불러다 쓸 수 있음
*/




// export const YoutubeApiContext = createContext();

// // const youtube = new FakeYoutube();
// const youtube = new Youtube();

// // context가 적용될 영역을 지정하는 함수 정의
// export function YoutubeApiProvider({ children }) {
//   return (
//     <YoutubeApiContext.Provider value={{youtube}}>
//       {children}
//     </YoutubeApiContext.Provider>
//   );
// }

// // youtube value값에 접근할 수 있도록 하는 함수(Videos.jsx)
// export function useYoutubeApi() {
//   return useContext(YoutubeApiContext);
// }

// /*
//   위와 같이 설정하면 youtube api 불러온 것을 모든 컴포넌트에서 불러다 쓸 수 있음
// */



  • util 폴더

util.js

import React from "react";
import { format, register } from "timeago.js";
import koLocale from "timeago.js/lib/lang/ko"; // n months ago를 한글로 표시

register("ko", koLocale);

export function formatAgo(data, lang = "en_US") {
  return format(data, lang);
}

// 단순한 시간을 n일 전과 같은 형식으로 바꿔주는 함수
// https://www.npmjs.com/package/timeago.js/v/4.0.0-beta.3



  • .env 파일
    .env
# .env - 외부에 공개되지 않는 파일
REACT_APP_YOUTUBE_API_KEY=내 키값;

출력

  • 이미지로 대체

fakeYoutube

youtube


🔗 참고 링크 & 도움이 되는 링크






profile
공부하는 벨로그

1개의 댓글

comment-user-thumbnail
2023년 8월 31일

안녕하세요. 드림코딩 운영자 입니다.
수강생분이 발견하여 신고가 들어와 블로그에 대해 알게 되었습니다.
해당 게시글과 블로그에 올리신 다수의 글들이 드림코딩 아카데미 유료 강의의 내용을 정리 하신걸로 확인됩니다.
이는 엄연히 저작권법 위반입니다.
해당 포스트들을 비공개 또는 삭제 처리후 info@dream-coding.com 로 메일 부탁드립니다.
시일내에 처리하지 않으시면 강의 취소 및 법적 대응 하겠습니다.
강의 시작전 저작권법에 관련해서 블로그에 정리하지 말아 달라고 안내해 드렸습니다.
https://academy.dream-coding.com/courses/player/react/lessons/1462

답글 달기