ㅇㅇㅈ Blog

프론트엔드 수행중

0%

React로 Movie-app 만들어 보기(1)

그 동안 배운걸 토대로
예전에 vue로 만들었던 영화검색 앱을 리액트로 만들어 보자..

컴포넌트 구성

App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// App.js

import Home from './routes/Home';
import Header from './components/Header';
import Movie from './routes/Movie';
import { BrowserRouter, Route, Routes, Router } from 'react-router-dom';

function App() {
return (
<>
<BrowserRouter>
<Header />
<Routes>
<Route path='/' element={<Home />} />
<Route path='movie' element={<Movie />} />
</Routes>
</BrowserRouter>
</>
);
}

export default App;

<Header/> 컴포넌트엔 페이지로 이동하는 버튼들이 있고
그 아래로 routes에서 각 페이지들을 보여주게 된다

Header.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Header.jsx

import React from 'react';
import { Link } from 'react-router-dom';
import './Header.scss';

const navigations = [
{
name: 'Home',
href: '/',
},
{
name: 'Movie',
href: 'movie',
},
];
export default function Header() {
return (
<header className='header'>
<div className='nav nav-pills'>
<div className='nav-item'>
{navigations.map((nav) => {
return (
<Link
to={nav.href}
key={nav.name}
className='btn btn-primary'
style={{ marginRight: 15 }}
>
{nav.name}
</Link>
);
})}
</div>
</div>
</header>
);
}

헤더 컴포넌트에 버튼이 늘어 날 수도있으니 데이터를 따로 만들어 주었다.
react-rounter-dom에서 제공하는 Link 컴포넌트는 html의 a태그 같은 것.
a태그를 사용하여 링크를 걸면 페이지가 새로고침이 일어 나기 때문이다.
아직 없는 페이지에 대한 처리를 하지 못했다..

Home.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react';
import MovieList from '../components/MovieList';
import Search from '../components/Search';
import Headline from '../components/Headline';

export default function Home() {
return (
<>
<div className='container'>
<Headline />
<Search />
<MovieList />
</div>
</>
);
}

일반적인 컴포넌트가 들어 있는 페이지

Search.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import React, { useState } from 'react';
import './Search.scss';
import { Form } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { _fetchMovie, fetchAsyncMovies } from '../store/features/searchSlice';

const filters = [
{
name: 'type',
items: ['movie', 'series', 'episode'],
},
{
name: 'number',
items: [10, 20, 30],
},
{
name: 'year',
items: (() => {
const years = [];
const thisYear = new Date().getFullYear();
for (let i = thisYear; i >= 1985; i--) {
years.push(i);
}
return years;
})(),
},
];

export default function Search() {
const [searchs, setSearchs] = useState({
title: '',
year: '',
type: '',
page: 10,
});
const dispatch = useDispatch();
const onChange = (e) => {
const { name, value } = e.target;
setSearchs({ ...searchs, [name]: value });
};
const onSubmit = (e) => {
e.preventDefault();
dispatch(fetchAsyncMovies(searchs));
};

return (
<>
<Form className='search--container' onSubmit={onSubmit}>
<Form.Control
type='text'
placeholder='Search Movies'
onChange={onChange}
name='title'
/>
{filters.map((select) => {
return (
<Form.Select
name={select.name}
key={select.name}
size='sm'
className='selects'
onChange={onChange}
>
{select.name === 'year' && (
<option value=''>{select.name}</option>
)}
{select.items.map((items) => {
return (
<option value={items} key={items}>
{items}
</option>
);
})}
</Form.Select>
);
})}
<button className='btn btn-primary'>Search</button>
</Form>
</>
);
}

Bootstrap을 이용해 form태그를 만들어 주었다
왜인지는 모르겠지만 bootstrap이 깔리지가 않아 한참 헤맸다..

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const filters = [
{
name: 'type',
items: ['movie', 'series', 'episode'],
},
{
name: 'number',
items: [10, 20, 30],
},
{
name: 'year',
items: (() => {
const years = [];
const thisYear = new Date().getFullYear();
for (let i = thisYear; i >= 1985; i--) {
years.push(i);
}
return years;
})(),
},
];

먼저 select에 들어갈 옵션값들을 데이터로 만들어 주었다
filters를 맵을 돌면 객체의 갯수만큼 select 태그가 만들어진다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{filters.map((select) => {
return (
<Form.Select
name={select.name}
key={select.name}
size='sm'
className='selects'
onChange={onChange}
>
{select.name === 'year' && (
<option value=''>{select.name}</option>
)}
{select.items.map((items) => {
return (
<option value={items} key={items}>
{items}
</option>
);
})}
</Form.Select>
);
})}

그리고 다시 select안의 item 갯수만큼 map을 돌면서 option들을 만들어주면 완성

그리고 요청 이벤트를 넣기 전에.. redux를 설치하고 store에 데이터와 action을 정의 해줬다

searchSlice.js

리덕스 사용법이 익숙치 않아 상당히 애를 먹었던 부분
store 파일만들고 provider로 감싸주는 부분은 생략

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// searchSlice.js

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const defaltMessage = 'Search for Movies...';

const initialState = {
movies: [],
message: defaltMessage,
loading: false,
theMovie: {},
};

const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {
_fetchMovie: async (state, { payload }) => {
const { title, type, year, page } = payload;
const url = `http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}`;

try {
const response = await axios.get(url);
console.log(response.data.Search);
return response.data.Search;
} catch (error) {
console.log(error);
}
},
},
});
export const { _fetchMovie } = searchSlice.actions;
export default searchSlice.reducer;

데이터는 받아 오는거 같지만 에러가 계속 뜨고
무언가 잘못된거를 느꼈다

강의에서 createAsyncThunk 라는 것을 언급하긴 했는데
자세한 설명은 없어서 무엇인지 잘 모르겠지만 이게 떠올랐다 - -
그리고 폭풍 검색…..

  • Redux Toolkit에는 내부적으로 thunk를 내장하고 있어서 다른 , 미들웨어를 사용하지 않고도 비동기 처리를 할 수 있다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';
const defaltMessage = 'Search for Movies...';

const initialState = {
movies: [],
message: defaltMessage,
loading: false,
theMovie: {},
};
export const fetchAsyncMovies = createAsyncThunk(
'search/fetchAsyncMovies',
async ({ title, type, year, page }) => {
if (initialState.loading) return;
const response = await axios
.get(
`http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}`
)
.then((res) => res.data);
return response;
}
);

const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {},
extraReducers: {
[fetchAsyncMovies.fulfilled]: (state, { payload }) => {
try {
state.loading = true;
state.movies = [...payload.Search];
state.message = '';
} catch {
state.movies = [];
state.message = payload.Error;
} finally {
state.loading = false;
}
},
},
});
// export const { _fetchMovie } = searchSlice.actions;
export default searchSlice.reducer;

무작정 작성했다.. 이해같은건 하지 못했다
특히 에러 처리부분에서 좀 헷갈렸다
createAsyncThunk 내부에서 에러 처리를 했더니 에러 문구 자체가 쪼개져서 배열로 들어가는 기괴한 일이 벌어졌다..
앞으로 중간과정 에러도 스샷을 남겨놓고 봐야겠다….

05.27 작업내용


fetchAsyncMovies , searchMovieWithID

  • fetchAsyncMovies 와 searchMovieWithID를 합쳐서 작업해보려 했으나 실패.
    • ID 값의 유무에 따라 각각 다른 데이터를 받아오는데 state에 어떻게 따로 넣을지 모르겠다 되는거긴 하는건가..
1
2
3
4
5
6
7
8
9
10
11
12
export const fetchAsyncMovies = createAsyncThunk(
'search/fetchAsyncMovies',
async ({ title, type, year, page, id }) => {
if (initialState.loading) return;
const url = id // id 유무값에 따라 쿼리를 다르게 보낸다
? `http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&i=${id}`;
: `http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}`
const response = await axios.get(url).then((res) => res.data);

return response;
}
);

id가 없이 요청했을때

id가 있는 요청일때

Movie Component

  • loading 값에 따라 <Loader /> 컴포넌트를 출력할건지 영화정보를 출력할건지 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
return (
<>
{loading ? (
<Loader />
) : (
<div>
<div
style={{
backgroundImage: `url(${requestdiffSizeimage(theMovie.Poster)})`,
width: 500,
height: (500 * 3) / 2,
backgroundSize: 'cover',
}}
>
{imageLoading && <Loader />}
</div>
{theMovie.Actors}
</div>
)}
</>
);
  • 쿼리로 받아오는 기본 이미지 사이즈는 sm 사이즈라 imgUrl 값에 포함된 사이즈 부분을 변경해주면 큰 사이즈 이미지를 받아 올 수 있다
1
2
3
4
5
6
7
const requestdiffSizeimage = (url, size = 700) => {
if (!url || url === 'N/A') {
setImageLoading(false);
}
const src = url.replace('SX300', `SX${size}`);
return src;
};

근데 잘 되다가 다시 확인하니 에러가 뜬다..

requestdiffSizeimage 함수가 실행 될때 아직 img를 못 받아와서 생기는 문제 인거 같다..
loading이 true가 되면 로딩화면이 보이고 false가 됐을때 img 및 정보들을 렌더 하게 했는데
loading은 true false 로 바뀌는데 로딩화면은 출력 되지도 않는다
비동기 처리를 어떻게 해야할지 감이 안잡힌다

05.28 작업내용

MovieList style 적용

message 출력 및 loader 출력 정리..

searchSlice refactoring

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

const defaultMessage = 'Search for movies..';

export const fetchAsyncMovies = createAsyncThunk(
'search/fetchAsyncMovies',
async ({ title, type, year, page }) => {
const url = `http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}`;
const response = await axios
.get(url)
.then((res) => res.data)
.catch((error) => error.message);
return response;
}
);

export const searchMovieWithID = createAsyncThunk(
'search/searchMovieWithID',
async ({ id }) => {
const url = `http://www.omdbapi.com/?apikey=${OMDB_API_KEY}&i=${id}`;
const response = await axios.get(url).then((res) => res.data);
return response;
}
);

const initialState = {
movies: {},
shows: {},
selectedMovieOrShow: {},
loading: null,
message: defaultMessage,
};
const searchSlice = createSlice({
name: 'search',
initialState,
reducers: {},
extraReducers: {
[fetchAsyncMovies.pending]: (state) => {
if (state.loading === true) return;
return { ...state, loading: true };
},
[fetchAsyncMovies.fulfilled]: (state, { payload }) => {
return { ...state, movies: payload, loading: false, message: null };
},

[searchMovieWithID.pending]: (state) => {
if (state.loading) return;
state.loading = true;
},
[searchMovieWithID.fulfilled]: (state, { payload }) => {
return { ...state, theMovie: payload, loading: false };
},
},
});
export const { loadingFuc } = searchSlice.actions;

export default searchSlice.reducer;

Movie component 에러

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
export default function Movie() {
const dispatch = useDispatch();
const theMovie = useSelector((state) => state.searchSlice.theMovie);
const params = useParams();

useEffect(() => {
dispatch(searchMovieWithID(params));
}, [dispatch]);

const requestdiffSizeimage = (url, size = 700) => {
if (!url || url === 'N/A') {
}
const src = url.replace('SX300', `SX${size}`);
return src;
};

return (
<>
{Object.keys(theMovie).length === 0 ? (
<Loader />
) : (
<div>
<div
style={{
backgroundImage: `url(${requestdiffSizeimage(theMovie.Poster)})`,
width: 500,
height: (500 * 3) / 2,
backgroundSize: 'cover',
}}
></div>
{theMovie.Actors}
</div>
)}
</>
);
}

loading 이 true 면 영화정보를 출력하게 했었는데 requestdiffSizeimage 함수가 실행 될때 영화정보가 아직 없는 상태라 에러가 났었다..
그래서 아예 영화정보의 length가 0 이 아닐 경우에만 출력하게 했더니 에러출력없이 잘 나온다..
❓ 무슨 차이가 있는걸까

또 다른 문제

이번엔 라우팅 문제

1
2
3
4
5
6
7
8
9
10
const navigations = [
{
name: 'Home',
href: '/',
},
{
name: 'Movie',
href: '/movie/tt2294629',
},
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
return (
<>
<BrowserRouter>
<Header />
<Routes>
<Route path='/' element={<Home />} />
<Route path='/movie/:id' element={<Movie />} />
<Route path='*' element={<NotFound />} />
</Routes>
</BrowserRouter>
</>
);
}

영화 정보를 요청하기전에 Movie 페이지를 누르면 아무것도 나오지 않아 임의로 아이디를 넣어 페이지가 출력 되게 해놨다
문제는 영화 정보를 요청하고나서 특정 영화의 id 값으로 params를 받아와 그 영화의 정보를 출력하는데
내가 선택한 영화가 출력되기 전에 임의로 부여한 id값의 영화가 잠깐 출력 되는 것..
그리고 Movie 버튼을 다시 누르면 페이지가 이동 되는 것..