Profiler 성능을 측정을 위해, 하나는 통으로 컴포넌트를 만들고, 하나는 각각 만들어 주었다. 처음 성능을 측정했을 때, 전자가 더 빠른 성능을 보여주었다. 검색창에 값을 입력해서, 억지로 컴포넌트를 다시 렌더링되도록 했을 때, 후자는 렌더링 되지 않아도 되는 컴포넌트까지 렌더링이 일어났기 때문이다.
이를 방지하기 위해, 컴포넌트 자체를 메모리제이션 해주는 React.memo를 통해 감싸주었다. props 가 동일할 때, 컴포넌트가 동일하기 때문에 다시 렌더링하지 않고 기억해놓은 컴포넌트를 바로 사용하는 방식이다. 따라서, 후자에서 재렌더링이 일어나지 않으면서, 더 좋은 성능을 보여주었다.
React Memo Props의 비교방식을 수정할 수 있었는데, props로 a,b,c,d가 주어졌을 때, a와 b만 이전 prop과 동일할 때 아래와 같이 함수를 작성했다.
function compareFunction(prevProps, nextProps) {
return (
prevProps.a === nextProps.a &&
prevProbs.b === nextProps.b
)
}
//true이면 메모이제이션 된 것을 사용하고,false의 경우 새로운 것을 사용한다.
단, 특정한 상황에서만 React.memo()를 사용해주는 것이 좋은데, component의 props가 웬만하면 바뀌지 않는 상황에서 사용하는 것이 좋다.
useCallback을 통해 컴포넌트가 다시 렌더링될 때마다 함수가 다시 생성되는 것을 막아주었다. ReactMemo와 같이 감싸주는 형태로 사용하는데, const testFunction = useCallback(() ⇒ { }, []); 의 형태로 사용해주었다. useEffect처럼, 뒤에 종속성 배열을 넣어주어, 배열이 바뀔 때에 함수를 새로 생성하도록 했다.
함수를 새롭게 만들고, list component에 props로 내려줄 때, 원래는 list component가 reactMemo로 감싸주었기 때문에 다시 렌더링이 되지 않았었는데, 함수를 내려주면서 그 때부터 list component가 다시 렌더링이 되었었고, test function에 useCallback을 넣어줌으로써 함수가 새롭게 생성되지 않아서, list component도 다시 렌더링이 일어나지 않았다.
<aside> 💡 useMemo를 통해 결과 값을 최적화하고, useCallback을 이용해 함수 값을 최적화하지만, 내부에서 일어나는 일은 동일하다고 할 수 있다. 다시 말해, useCallback은 메모이제이션된 함수를 반환하고, useMemo는 메모이제이션된 값을 반환한다.
</aside>
예를 들어, 메모이제이션은 다음과 같은 사고를 거쳐 이루어진다.
function Component({ a,b }) {
const result = compute(a,b)
return <div> {result} </div>
}
//compute함수에서 heavy한 계산을 다시 해주는 것은 비효율 적이므로,
//a.b가 값이 바뀌지 않는 이상 결과값이 바뀌지 않는다.
function Component({a,b}) {
const result = useMemo(() => compute(a,b), [a,b])
return <div> (result) </div>
} //useMemo를 사용하면, a,b가 바뀌어야 결과값이 바뀌게 된다.
The Movie DB API를 생성할 때, 생성한 api key를 받고, api_url을 통해 영화의 상세정보를 받아온다. api 서버에 명시된 방식을 따라서 요청을 보내게 되는데, API 요청을 위해 Axios 모듈을 사용해서 보내는데, HTTP 비동기 통신 라이브러리이다. 서버에 요청을 보낼 때 fetch 메소드를 사용해서 보내는 경우도 있다. Axios를 인스턴스화해서(인스턴스 생성 후 요청을 보낼 때) 중복된 부분을 계속 입력하지 않도록 했다. 정보를 보내기 위해, api 키값, languge를 설정하고, request에 url을 넣어주었다.
disney 앱을 만들때 어떤 구조로 만들지 생각해야 하는데, nav바를 스크롤 시 색을 변경하는 부분을 구현했다. styled component를 이용하여, 스크롤 이벤트가 발생했을 때 state를 변경해서 state 값에 따라(true, false값에 따라) 색을 변경해주었다.
main page의 container를 생성하여 전체적인 UI를 구성하고 이미지 배너를 생성해주었다.
#App.js 에서 <Banner /> 배너 컴포넌트를 가져온다.
#Banner.js
import axios from '../api/axios';
import React, { useEffect, useState } from 'react'
import requests from '../api/requests'; //default
import "./Banner.css";
import styled from 'styled-components';
const Banner = () => {
//컴포넌트가 마운트되면 가장 먼저 실행되는 부분
//밑에서 error가 난이유는 state에 아무 값도 넣어주지 않았기 때문이다. 따라서 movie에 undefine이 할당이 된다.
const [movie, setMovie] = useState(); //컴포넌트에서 movie 데이터를 기억해주기 위해 사용
const [isClicked, setIsClicked] = useState(false); //초기값은 false
useEffect(() => {
fetchData();
}, []) // 요청을 보내기 위해 useEffect를 사용한다.
//아래 함수 호출
const fetchData = async () => { //비동기 요청을 보내기 위해 async로 감싸준다.
try {
// 현재 상영중인 영화 정보 가져오기
// axios 인스턴스 내부에 baseURL과 param이 들어있으므로
// axios를 모듈에서 가져오지 않고 인스턴스로 가져온다.
// requests의 경로를 이용해서 fetchNowPlaying 객체를 넣어준다.
// 여러개의 데이터 중에서 backdroppath 값 (이미지)를 가져온다.
const response = await axios.get(requests.fetchNowPlaying)
// 여러 영화 중 랜덤으로 영화 하나의 ID를 가져오기
//20개의 데이터 중에서 랜덤으로 id값을 가져오기 위해 floor, random을 사용한다.
//새로 고침하면 계속 랜덤으로 영화를 가져온다.
const movieId = response.data.results[Math.floor(Math.random() * ~~request~~ response.data.results.length)].id;
// 특정 영화의 더 상세한 정보를 가져오기
// append_to_response가 response에 "videos" 데이터를 더해준다는 의미로, 새롭게 출력시 video에 대한 정보가 있으면 값이 출력된다.
// 구조분해 할당을 이용하여 { data: movieDetail(가져올 때 이름) }로 결과값 출력시 response가 아닌 movieDetail값으로 가져온다.
const { data: movieDetail } = await axios.get(`movie/${movieId}`, {
params: { append_to_response: "videos" }
})
console.log(~~response~~ movieDetail)
setMovie(movieDetail); //setMovie를 이용해서 moveDetail 가져온 데이터들을 movie라는 state에 저장한다.
//값이 들어가면 오류 발생 x
} catch (error) {
console.error(error);
}
}
if (!movie) return <div>로딩중</div> //값이 없어서 생기는 에러 해결
//아래 return 이후의 소스코드가 걸리기 전에 띄울 수 있도록 이 위치에 작성!
//해결 시 로딩중이 깜빡이면서 데이터를 가져온다.
//오류가 발생한다면, api key값, 그리고 요청경로(console창의 network 탭에서 now_playing?api_key...의 General탭의 Request URL 확인!)
if (!isClicked) { //isClicked가 false일 때 바로 아래의 return 반환값을 보여준다. (비디오 화면 x)
return ( //가져온 데이터를 가지고 UI를 구성한다.
<header
className='banner' //class이름은 banner
style={{
backgroundImage: `url("<https://image.tmdb.org/t/p/original${movie.backdrop_path}>")`, //이미지 경로는 앞부분은 동일한 URL을 가지고, 뒷부분만 unique한 값을 가진다. movie의 backedroppath 값을 경로에 넣어준다.
//movie 가 undefine인데 값을 가져오려고 하니까 error가 발생함.
backgroundPosition: "top center",
backgroundSize: "cover" //사이즈는 커버 사이즈
}}
>
<div className='banner__contents'>
<h1 className='banner__title'> // 제목
{movie.title || movie.name || movie.original_name}
//movie title이 없으면, name을, 없으면 original_name을 가져온다. '||' = 또는!
</h1>
<div className='banner__buttons'>
{
movie.videos?.results[0]?.key ?
// movie가 있으면 가져오고 없으면 안가져온다.(undefined로 발생하는 오류 방어코드)
// 아래 사진과 같이, key값이 있으면? 아래 값을 가져온다.
<button
className='banner__button play'
onClick={() => setIsClicked(true)} //버튼 클릭시 true로 바뀌면서 비디오 화면으로 전환
>
Play //화면의 play 버튼
</button>
: null //업으면 null 값 반환
}
</div>
<h1 className='banner__description'> //영화 설명글 <p>태그로 넣어주어도 된다.
{truncate(movie.overview, 100)} //데이터 중에서 movie아래의 overview(설명글 데이터)를 가져온다.
</h1>
</div>
<div className='banner--fadeBottom' />
</header>
)
} else {
return ( //isClicked가 true 일 때 보여주는 비디오 화면
<>
<Container>
<HomeContainer>
<Iframe
src={`https://www.youtube.com/embed/${movie.videos.results[0].key}?controls=0&autoplay=1&mute=1&loop=1`}
> //올바른 비디오 경로 넣기 (share-> embeded -> share), 통째로 가져오면, width 값과 height 값때문에 작게 나온다.
//값에 맞는 비디오를 넣기 위해, 데이터에서 key값을 확인한다.
// controls=0&autoplay=1&mute=1&loop=1 부분은 autoplay가 가능하도록 하고, mute 음소거되도록 하고, loop 반복이 가능하도록 한다는 의미.
</Iframe>
</HomeContainer>
</Container>
<button onClick={() => setIsClicked(false)}>
X
</button>
// 중간에 button을 2개 넣으면 안되는 이유는 jsx문법에서 부모 컴포넌트(<> , </>)로 자식 컴포넌트를 감싸주어야 하기 때문이다.
// 그리고, button 이 클릭할 때 기능하게 하려면 위의 조건이 true로 주어졌으므로, setclick을 사용하여 기능하게 해야 한다.
//실제로 video가 재생되면 왼쪽 아래에 x버튼으로 다시 banner로 돌아올 수 있다.
</>
)
}
}
//styled-components
const Iframe = styled.iframe`
width: 100%;
height: 100%;
z-index: -1;
opacity: 0.65;
border: none;
`//화면에 iframe 요소를 꽉채우기 위해 작성
const Container = styled.div
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100vh;
`;
//iframe을 container로 감싸주기 위해서 작성
const HomeContainer = styled.div`
width: 100%;
height: 100%;
`
export default Banner


# css 파일
.banner {
color: white;
object-fit: contain;
height: 448px;
}
@media (min-width: 1500px) {
.banner {
position: relative;
height: 600px;
}
.banner--fadeBottom {
position: absolute;
bottom: 0;
width: 100%;
height: 40rem;
}
}
@media (max-width: 768px) {
.banner__contents {
width: min-content !important;
padding-left: 2.3rem;
margin-left: 0px !important;
}
.banner__description {
font-size: 0.8rem !important;
width: auto !important;
}
.info {
text-align: flex-start;
padding-right: 1.2rem;
}
.space {
margin-left: 6px;
}
.banner__button {
font-size: 0.8rem !important;
border-radius: 4px !important;
}
}
.banner__contents {
margin-left: 40px;
padding-top: 140px;
height: 190px;
}
.banner__title {
font-size: 3rem;
font-weight: 800;
padding-bottom: 0.5rem;
}
.banner__description {
width: 45rem;
line-height: 1.3;
padding-top: 1rem;
font-weight: 500;
font-size: 1rem;
max-width: 400px;
height: 80px;
}
.banner--fadeBottom {
height: 7.4rem;
background-image: linear-gradient(180deg,
transparent,
rgba(37, 37, 37, 0.61),
#111);
}
.banner__buttons {
display: flex;
flex-direction: row;
}
.banner__button {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
cursor: pointer;
outline: none;
border: none;
font-size: 1rem;
font-weight: 700;
border-radius: 0.2vw;
padding: 0.4rem 1rem;
margin-right: 1rem;
}
.banner__button:hover {
color: #000;
background-color: rgba(170, 170, 170, 0.9);
transition: all 0.2s;
}
.play {
background-color: white;
color: black;
}
.space {
margin-left: 4px;
}