이번에는 search 헤더를 만들 것이다.
* favicon.ico는 ico 라는 것은 폴더를 열어서 보면, 다양한 사이즈가 묶여져 있는 파일이다
브라우저나 모바일, 데스크탑 상관 없이 원하는 아이콘을 골라서 사용할 수 있다.
https://convertio.co/kr/png-ico/
PNG ICO 변환 (온라인 무료) — Convertio
png 파일(들) 업로드 컴퓨터, Google Drive, Dropbox, URL에서 선택하거나 이 페이지에서 드래그하여 선택해 주세요.
convertio.co
이렇게 png - ico 로 변환하는 사이트도 있다.
png를 골라서 ico로 바꾸면 된다.
search header는 video 위에 보여져야 한다. (맨 상단에)
일단 기본적인 search header.jsx 파일과 css 파일을 만들어준다.
이렇게 css와 jsx 파일을 만들고, jsx 파일에서 css 파일을 import 한다.
rsi를 이용해서 search header를 만들어 준다.
그리고 <h1>태그로 Header를 작성한 후 (테스트 용) 저장을 해주고,
app.jsx에 return 할 때, 맨 앞에 보여져야 하므로, videolist 위에 searchheader를 import 해서 가져온다.
그럼 브라우저 상에서 이렇게 표현된다.
이제 우리는 search header를 만들어야 한다.
이렇게 search header 코드를 작성해준다.
.header {
display: flex;
height: 4rem;
padding: 0.8em 1em;
background: black;
color: white;
}
.logo {
display: flex;
align-items: center;
margin-right: 1em;
}
.input {
flex-basis: 100%;
font-size: 1.2rem;
outline: 0;
}
.button {
background-color: darkgray;
outline: 0;
}
.buttonImg {
height: 100%;
padding: 0.5em 0.2em;
}
css는 이렇게 작성해준다.
이제 검색 버튼이 클릭이 되면, 클릭한 것을 처리해주고, input 필드에서 엔터를 누르면 처리가 되게 해주어야 한다.
만들었던 search_header.jsx 코드의 input과 button 부분에 이벤트를 처리해준다.
이렇게 input 태그 안에 onKeyPress를 넣고, 함수를 선언해서 처리를 해주고,
button 태그 안에 onClick 을 넣어서 함수를 선언해서 처리를 해준다.
그리고 키가 눌러졌을 때, 검색 버튼(아이콘)을 클릭했을 때 우리가 처리해야 할 부분은 똑같다.
그래서 함수를 새로 만들어서 처리해야 할 부분에 관한 기능을 콜백함수로 만들어준다.
const handleSearch = () => { } 콜백 함수를 만들어 준다.
이렇게 만든 콜백함수가 작동해야 할 기능은, 일단 우리가 input에 입력한 값을 알아야 하기 때문에
habit 에서 배웠던 inputRef를 사용해야 한다.
react hook에서는 useRef();를 이용해야한다.
const inputRef = useRef(); 를 정의해준다.
그 후 input 태그 안에 ref={inputRef} 로 연결해주면 된다.
그리고, 이 만들어 준 handleSearch 함수를 onClick 에 넣고, onKeyPress에도 넣는다.
onKeyPress는 우리가 'Enter' 키 일때만 이벤트가 발생할 수 있도록 조건을 걸어준 후 handleSearch 를 넣어준다.
const onClick = () => {
handleSearch();
}
const onKeyPress = event => {
if (event.key === 'Enter') {
handleSearch();
}
}
이렇게 처리해준다.
이제 const handleSearch 에는 input에 입력한 현재의 값을 가져와야 한다.
const handleSearch = () => {
const value = inputRef.current.value;
}
이렇게 우선 현재의 값을 가져온다.
우리가 이 컴포넌트 안에서 검색을 할 수 있는 것이 아니라, 검색을 하는 것을 props로 받아와야 한다.
우선 const SearchHeader 자체에서 props에 onSearch를 받아온 후, 일단 검색이 되면
onSearch안에 현재의 value 값을 넣어주어야 한다.
이렇게 props를 onSearch로 받아오고 handleSearch 안에 onSearch(현재 값) 을 인자로 넣어주면,
props 에서 검색 기능을 처리해 줄 것이다.
이제 searchHeader 를 선언했던 props의 app.jsx 로 가서 onSearch 작업을 해주어야 한다.
이제 app.jsx 에서 검색 기능을 만들어주어야 한다.
검색기능을 만들어주는 방법은, 일단 아까 List를 가져온 것 처럼 search도 미리 만들어놓은 postman에 code를 가져온다.
app.jsx
function App() {
const [videos, setVideos] = useState([]);
const search = query => {
const requestOptions = {
method: 'GET',
redirect: 'follow',
};
fetch(`https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResult=25&q=${query}&key=KEY`, requestOptions)
.then(response => response.json())
.then(result => setVideos(result.items))
.catch(error => console.log('error', error));
return (
<SearchHeader onSearch={search}/>
이렇게 postman 에서 api를 가져와서 query를 인자값으로 받고 아까 전의 current value 가 여기 인자값으로 리턴해주었기 때문에 그 값이 들어가면, 그것이 query가 되고, 그 query를 fetch의 url param 값에 넣어 준 것이다.
그것을 setVideos로 넘겨주었기 때문에 video가 정상적으로 나오는 것을 볼 수 있다.
이렇게 나오는 것을 볼 수 있다.
지금 우리는, video의 list와 지금 가져온 search의 api가 조금은 다른 것을 확인할 수 있다.
video의 list는 items 안에 id가 그냥 있는데, search 의 api의 id는
지금 이렇게, id 안에 videoId가 따로 담겨있는 것을 볼 수 있다. 이것으로 인해 충돌이 발생한다. (object 형태로 받아오기 때문)
일단 postman 에서도 type을 video로 지정해주도록 해야한다 (params을 바꾸면 됨)
우리는 이 id를 임의로 지정해주어야 한다.
fetch(`https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResult=25&q=${query}&key=KEY`, requestOptions)
.then(response => response.json())
.then(result => result.items.map(item => ({...item, id: item.id.videoId})))
.then(items => setVideos(items))
.catch(error => console.log('error', error));
우리는 이렇게 item 각각의 값을 map으로 배열복사해주고, 다만 id 값을 따로 videoId로 지정을 해주면 된다.
지금 이렇게 해준 것이, 아까전의 result.items 와 똑같은 구조일 것이다. object 값으로 넘어가지 않도록 id를 임의로 지정해준 것이다.
이렇게 하면 검색을 했을 때, 충돌이 나지 않고 깔끔하게 데이터가 보여질 수 있다.
사실 이렇게 api를 사용할 때, app 컴포넌트에서 문제점이 꽤 있다.
리액트 라는 것은 MVC를 만들 때 사용하는 디자인 패턴이다. 안드로이드에서는 MVVM을 사용하고 MVI도 있고 MVP 도 있고, 다양한 디자인 패턴이 있는데, 이런 디자인 패턴은 우리가 해결하고자 하는 문제는 딱 하나이다. 어플리케이션에서 조금 더 역할에 맞게 세부적으로 레이어를 나누어서 한가지의 것이 한가지의 역할을 가질 수 있도록 세분화해서 나눔으로서 테스트도 쉽고 유지보수도 개발도 더 쉽게 할 수 있는 것이다.
리액트는 View 레이어를 담당한다. View 레이어는 사용자에게 데이터를 보여주고 클릭이되면 클릭 이벤트 자체를 처리하는 view 에 관련된 것만 해야 한다.
그래서 이 View는 네트워크도, 비즈니스 로직도 그렇게 처리할 수 있게 똑똑하게(?) 만들면 안된다.
그래서, app.jsx 컴포넌트 안에는 문제점이 있다.
1. key 값이 중요한 키가 코드안에 들어있다 (깃허브에 올리면 다른 사람이 보니까 좀 별로다)
2. 컴포넌트안에 api 통신이 대놓고 들어있다. (view의 역할이 흐려진다)
이렇게 코드를 작성하게 되면나중에 컴포넌트를 유닛테스트 하기 위해서는 네트워크 통신하는 것도 검사해야한다. 그렇다면 유닛테스트를 할 때, 네트워크 통신 할 때까지 기다려야한다 (매우 안좋음) 유닛테스트를 돌릴때마다 네트워크 통신이 돌아간다.
그래서 우리는 네트워크 통신 하는 부분을 따로 모아서 class를 만들어 놓고, class 자체에 필요한 것을 컴포넌트 안에 주입해주면 (defendancy injection)
나중에 유닛테스트 할 때는 네트워크 통신할 때 그냥 더미데이터(가짜데이터같은거) 를 대충 전달해주면 조금 더 유닛테스트가 쉽고 빠르게 된다.
이렇게 components 가 아닌 service 파일을 따로 만들어서, youtube.js (순수 js파일임) 를 추가해준다.
이렇게 만들어진 youtube.js 파일에, youtube class를 만들고, 일단 key를 인자로 받고 생성자를 생성한다.
class Youtube {
constructor(key) {
this.key = key;
this.getRequestOptions = {
method: 'GET',
redirect: 'follow',
};
}
그 후, 우리는 api를 mostPopular와 search 가 있으므로 이것을 class 안의 메소드로 나누어서 작성한다.
class Youtube {
constructor(key) {
this.key = key;
this.getRequestOptions = {
method: 'GET',
redirect: 'follow',
};
}
mostPopular() {
return fetch(
"https://youtube.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&maxResults=25&key=AIzaSyDayJ41-72DoM1-EFXhsVW9nYEvFYQwSbA",
this.getRequestOptions
)
.then((response) => response.json())
.then((result) => result.items)
}
search(query) {
return fetch(
`https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResult=25&q=${query}&type=video&key=AIzaSyDayJ41-72DoM1-EFXhsVW9nYEvFYQwSbA`,
requestOptions
)
.then((response) => response.json())
.then((json) =>
json.items.map((item) => ({ ...item, id: item.id.videoId }))
)
}
}
이렇게 app.jsx 에 있었던 api 들을 youtube.js 로 불러왔다.
그러면 app.jsx 에서는 이제 youtube.js를 import 한 다음 클래스를 사용하듯 해야한다.
하지만 유튜브 클래스를 선언하고 가져와서 사용할 때, 이렇게 search 나 이런 안에서, 이런 부분에 사용하는 것이 아니다.
이렇게 되면 함수를 호출할 때 마다 새로운 youtube 를 계속적으로 반복적으로 만드는 것이 굉장히 안좋고,
그리고 이 youtube에 대해 알고 있고 어떻게 하는 지 알고 있는데, 여기서 네트워크 통신을 하게 된다면 어차피 유닛테스트할 때도 네트워크 호출이 계속 일어나게 된다.
그래서 우리는 youtube 라는 것을 props로 받아와야 한다.
app을 쓰는 최종적인 props은 index.js 이므로 index.js에서 props을 설정해서 받아오는 방법으로 사용해야 한다.
index.js
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./app";
import Youtube from "./service/youtube";
const youtube = new Youtube('key값을 인자로 받는다');
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App youtube={youtube} />
</React.StrictMode>
);
이렇게 index.js 에서 youtube 를 생성해준다.
그리고 사용하는 App에서 youtube를 props로 넘겨준다.
이제 이 youtube는 index.js가 호출되는 딱 한번만 만들어진다. (이 만들어진 것을 갖다 쓰는 것이다.)
이제 유닛테스트 같은 것을 할 때는, 네트워크 통신을 딱히 하지 않아도 이미 그냥 만들어 놓아져있는 클래스를 주입해서 간단하게 테스트해볼 수 있다.
이렇게 index에서 app으로 넘겨왔다면,
app.jsx 에서
function App({ youtube }) {
const [videos, setVideos] = useState([]);
const search = query => {
youtube.search(query)
.then(videos => setVideo(videos));
}
useEffect(() => {
youtube.mostPopular()
.then(videos => setVideo(videos));
}, []);
이렇게 api 키도 안보이게 깔끔하게 받아와주면, app.jsx 가 좀 더 간결하게 리팩토링 된 것을 볼 수 있다.
이제 KEY 부분을 안보이게 해야한다.
이제 youtube.js 에서 Key를 this.key로 바꿔 온 다음,
이제 index.js에서 인자 값에 key를 전달해주면 된다.
** 하지만 이 기본적인 키는 깃허브에도 올라가면 안되고, 기본적으로 코드 자체에 들어가면 안된다.
따로 환경변수 env로 빼주어야 한다.
https://create-react-app.dev/docs/adding-custom-environment-variables
Adding Custom Environment Variables | Create React App
Note: this feature is available with react-scripts@0.2.3 and higher.
create-react-app.dev
어떻게 환경변수 env를 쓸 수 있는 지 공식문서에 나와있다.
제일 상위에 .env 파일을 만들어준다.
그런다음, 변수를 이렇게 REACT_APP_YOUTUBE_API_KEY 를 설정하고, key를 복사해서 붙여넣는다.
이런 변수를 index.js에서 사용하면 된다.
const youtube = new Youtube(process.env.REACT_APP_YOUTUBE_API_KEY);
이렇게 호출하면 된다.
이제 git에 실수로 커밋하면 안되기 때문에 gitignore 에 .env를 깃허브에 추적되지 않도록 한다.
***
여기서 promise 말고 async 로 해도 좋다.
async mostPopular() {
const response = await fetch(
`https://youtube.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&maxResults=25&key=${this.key}`,
this.requestOptions
);
const result = await response.json();
return result.items;
}
async search(query) {
const response = await fetch(
`https://youtube.googleapis.com/youtube/v3/search?part=snippet&maxResult=25&q=${query}&type=video&key=${this.key}`,
this.requestOptions
);
const result = await response.json();
return result.items.map((item) => ({ ...item, id: item.id.videoId }));
}
'React' 카테고리의 다른 글
react youtube-clone 목록으로 돌아가기 (0) | 2022.09.29 |
---|---|
react youtube-clone 비디오 선택 & 상세화면 (0) | 2022.09.29 |
react youtube-clone videoList (MostPopular) 목록 api 이용해서 리스트 아이템 만들기 (0) | 2022.09.28 |
react youtube-clone postman 소개와 설정 (0) | 2022.09.28 |
react youtube-clone youtube apis 살펴보기 (0) | 2022.09.28 |