영화검색
-
Store의 Mutations를 실행할 때는 .commit()
메소드를, Actions를 실행할 때는 .dispatch()
메소드를 사용한다.
-
변이 메소드 (updateState)
export default {
namespaced: true,
state: () => ({
movies: [],
mesaage: '',
loading: false
}),
getters: {},
mutations: {
updateState(state, payload) {
// ['movies', 'message', 'loading']
Object.keys(payload).forEach(key => {
state[key] = payload[key]
})
}
},
actions: {
async searchMovies(context, payload) {
const { title, type, number, year } = payload
const OMDB_API_KEY = '키값입력'
const res = await axios.get(`https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}&page=1`)
const { Search, totalResults } = res.data
context.commit('updateState', {
movies: Search,
message: 'Hello World!',
loading: true
})
}
}
}
actions 부분에서 데이터를 갱신하기 위해서 mutations에 있는 updateState를 commit 메소드를 통해서 실행할 수 있다.
실행할 때, 기본적인 객체 데이터(moives: Search …
)를 담아서 전달을 해줄 수 있다.
이것은 mutations의 payload 변수로 전달이 된다. 이, payload는 하나의 객체 데이터이며, Object.keys
를 통해서 배열 데이터로 만든다.
배열 데이터의 갯수만큼 forEach()
로 반복한다. 한번 반복할 때마다, 첫번째 key는 movies이고, 이때 state에 있는 movies(state[’movies’]
)라는 데이터의 payload에 있는 movies (payload[’movies’]
) 데이터 즉, (actions의 commit 객체 데이터) Search라는 OMDb API에서 가지고 온 데이터를 payload에서 state로 할당해줄 수 있다.
두번째 반복이 될 때는, key가 message이고 state[’message’]
는 payload[’message’]
즉, ‘Hello World!’
문자 데이터로 할당한다.
세번째도 마찬가지.
updateState라는 공용화하여 사용할 수 있는 로직을 만들어 둔다면, 어디에서든지 저장소에 있는 데이터를 변이 메소드를 통해서 수정할 수 있다.
영화 검색 추가 요청
첫번째 페이지에는 총 10개의 영화가 검색되어 출력된다.
우리가 20개, 30개까지 검색이 가능하도록 했는데, 10개가 넘어가면 2페이지, 3페이지로 넘어가면서 검색이 된다.
우리는 처음 검색한 1페이지에서 추가로 스크롤하면 다음 검색 페이지가 이어서 나오도록 만들 것이다.
const total = parseInt(totalResults, 10)
const pageLength = Math.ceil(total / 10)
pageLength는 우리가 검색한 키워드를 모두 출력하고자 할때, 한 페이지에 결과를 10개씩 요청했을 때 반복해야 하는 횟수이다. 우리가 검색한 결과의 갯수에서 10을 나누어 올림해준다.
// 추가 요청
if (pageLength > 1) {
for (let page = 2; page <= pageLength; page += 1 ) {
const res = await axios.get(`https://www.omdbapi.com/?apikey=${OMDB_API_KEY}&s=${title}&type=${type}&y=${year}&page=${page}`)
const { Search } = res.data
commit('updataState', {
movies: [...state.movies, ...Search]
})
}
}
pageLength, 즉 우리가 추가 검색을 요청하려는 횟수가 1 이상일때 추가 요청이 발생한다.
기본은 page가 1부터 시작하니까 우리가 다음로 찾아야 하는 시작 페이지는 2페이지이다. 따라서 변수 page는 2부터 시작한다. page가 pageLength보다 작거나 같을때 까지만 반복하도록 한다.
추가 요청이 들어왔으니 res 변수를 다시 호출해서 API를 불러온다.
get 메소드 안의 마지막 줄에 page=1이었으나, 추가 요청을 함에 따라 페이지의 숫자가 증가하니 ${page}
로 변경해준다. 요청이 늘어날때 마다 증가하는 변수(page)의 값이 들어간다.
movies에는 최초의 요청을 통해서 들어온 첫 페이지의 10개의 영화목록이 존재한다. 추가적으로 응답이 온 데이터를 movies 데이터 뒤쪽으로 밀어넣어 줘야 한다.
commit 메소드를 실행하여 updateState 변이 메소드를 실행해준다. movies 데이터를 수정한다고 선언한다.
추가적으로 들어온 Search를 바로 movies에 할당하면 기존에 존재했던 데이터들은 덮어져서 사라진다. movies: Search
따라서, 빈 배열 데이터를 만들어주고 내부에서 전개 연산자(…
)를 실행해서 state에 있는 기존의 movies 데이터를 먼저 전개해준다.
다음에 추가로 가지고온 Search도 전개연산자를 통해서 뒤쪽에 새롭게 데이터를 전개해준다. 이렇게 되면 새로운 배열로 movies에 배열을 할당하게 된다.
movies: [...state.movies, ...Search]
추가로, state 오류가 발생하는데 searchMovies에 state 사용 선언이 안되어있기 때문에 context 부분에 state 사용을 선언해주면 된다.
async searchMovies({ state, commit }, payload)
중복 ID 제거 (Lodash: uniqBy)
Lodash Documentation: uniqBy
lodash의 uniqBy라는 API 명령을 사용한다.
특정한 속성의 이름을 가지고 배열 데이터를 고유화 시켜줄 수 있는 API이다.
배열 데이터안에 들어있는 각각의 객체 데이터의 특정한 속성 이름을 기준으로 하여 배열 데이터를 고유화 시켜주는 기능을 가진다.
x라는 속성의 값이 중복되고 있는 배열 데이터가 첫번째 인수이며, 두번째 인수는 속성의 이름이다. uniqBy가 두번째 인수의 속성 이름을 확인해서 중복되는 부분들을 제거해준다. 결과적으로 중복이 제거된 새로운 배열데이터가 반환이된다.
Vue로 페이지를 제작한 후, 기능이 동작하지 않는 현상이 발생했다..
일단 문제가 생겼으니 console 탭을 확인하니 다음과 같은 warn 표시가 발생했다.

해석해보면 moives라는 property는 정의되지 않았다. 뭐 이런 뜻인데..
어디선가 잘못 작성한게 확실했다.
하단에 후보군도 나와 있으니 모두 뒤져봤으니 나오지 않았다..!!!
그렇게 헤매다가 다시 콘솔탭을 확인했더니, 웬걸..? movies가 아닌 moives 였던것…!!!
moives로 단어 검색을 해보니 오타가 발생해 있었다..
그렇다. 해당 에러는 대부분 오타에서 발생한다.. 작성할 때 주의하도록 하자..ㅜ
Vuex: 데이터를 한 곳에서 처리하기
Search 부분과 MovieList 부분의 형제컴포넌트 관계이다.
Home.vue에서 사용한 컴포넌트를 확인하면 Search 컴포넌트와 MovieList 컴포넌트는 같은 라인에 존재하는 형제 컴포넌트임을 확인할 수 있다.
부모와 자식관계에서는 props와 emit을 사용하고, 상위와 하위 컴포넌트 관계(부모,자식을 넘어선)에서는 provide와 inject 기능을 통해 데이터를 통신한다.
그렇다면, 형제 컴포넌트의 경우에는 어떻게 데이터를 통신할까?
-
Vuex
데이터를 한 곳에 몰아둔 다음에 데이터를 통신하도록 한다. 해당 방법은 Vuex를 사용한다.
Vuex는 중앙 집중식 상태관리 라이브러리이다.
통상적으로 해당 방법들을 store라고 부른다.
-
현재 프로젝트 상태

해당 이미지는 영화 검색 사이트의 상태를 나타내고 있다.
우리는 Search 컴포넌트를, Movie 컴포넌트와 MovieList, MovieItem 컴포넌트에서 활용을 할 것이다.
Search는 형제 컴포넌트인 MovieList와 데이터 통신을 해야 할 뿐 아니라, 복잡한 관계인 Movie 컴포넌트와도 데이터를 통신해야 한다.
이런 복잡한 관계들에서 데이터 통신을 보다 편하게 사용하도록 store(Vuex)를 사용한다.
-
Store를 사용한 상태

프로젝트에, 데이터를 중앙집중화(store)할 수 있는 장소를 만든다.
스토어에 다이렉트로 해당하는 컴포넌트들의 데이터를 연결해서 사용한다.
관계가 복잡해지더라도 중간 매개체(스토어)를 단 하나만 사용하면 된다는 편리함이 있다. 또, 하나의 매개체를 통해서 프로젝트 내부에 있는 어떤 곳의 컴포넌트에도 데이터를 전달할 수 있다는 장점이 있다.
-
모듈
우리는 스토어에 영화검색만 할 것이 아니라 about 페이지에서 사용할 기본적인 개발자 정보도 스토어에서 관리할 것이다.
스토어에서 관리할 데이터의 종류는 영화의 목록(movie), 영화의 상세 정보(movie), 개발자 정보(about)가 있다. 해당 정보들은 결이 다른 정보들이기 때문에 별도의 모듈(movie, about)로 분리해서 스토어로 관리할 것이다.
모듈로 분리하는 이유는 프로젝트가 커질 수록 취급해야 하는 데이터의 종류들이 달라질 수 있고 훨씬 많은 종류들이 붙을 수 있기 때문에 모듈화를 한다.
-
상태
스토어의 개념에서는 데이터를 상태라고 부른다.
따라서 중앙 집중식 상태 관리, 데이터를 관리해주는 하나의 개념이며 패턴이라고 부른다.
정리하면 중앙 집중식 상태 관리 패턴 라이브러리이며, 우리가 사용할 라이브러리가 Vuex인 것이다.
Vuex: store 구성
Vuex4: What is Vuex?
Vuex3: Vuex가 무엇인가요?(한글)
-
vuex 설치
-
store 생성
src 폴더 내부에 store 폴더 생성, 내부에 index.js 파일 생성
-
main.js에 index.js 파일 연결
import { createApp } from 'vue'
import App from './App.vue'
import router from './routes/index.js'
import store from './store/index.js'
createApp(App)
.use(router)
.use(store)
.mount('#app')
추가 Tip!!
특정한 폴더에 있는 index라는 이름을 가진 파일은 생략이 가능하다!!
폴더 이름만 작성하면 index라는 이름의(특히 자바스크립트 파일)의 파일을 우선 적용하는 구조이기 때문이다.
따라서, 폴더에서 기본적으로 사용하는 파일의 이름을 정할때는 index 이름을 사용하는 것이 좋다.
import { createApp } from 'vue'
import App from './App.vue'
import router from './routes'
import store from './store'
createApp(App)
.use(router)
.use(store)
.mount('#app')
-
store 폴더에 movie, about 파일 생성
-
movie.js 파일
영화 검색과 관련된 데이터를 취급한다.
export default {
// module
namespaced: true,
// data
state: () => ({
movies: []
}),
// computed
getters: {
movieIds(state) {
return state.movies.map(m => m.imdbID)
}
},
// methods
// 변이
mutations: {
resetMovies(state) {
state.movies = [] // 빈 배열로 초기화
}
},
// 비동기
actions: {
searchMovies() {
}
}
}
-
namespaced
movie.js가 하나의 스토어에서 모듈화돼서 사용될 수 있다는 것을 나타내는 옵션이다.
값으로 true를 넣게되면, moive.js를 index.js의 modules 부분에 명시해서 별개의 개념으로 활용할 수 있다.
-
state
실제로 취급해야 하는 각각의 데이터들을 의미한다.
데이터를 상태(state)라고 부른다!
-
getters
계산된 상태를 만들어낸다.
계산된 데이터를 만들어내는 computed와 동일하다고 보면 된다.
즉, 데이터는 상태(state)이고 comptued는 getters.
-
mutations, actions
methods와 유사하다.
해당 옵션 부분에 함수를 만들어서 movie.js에 있는 여러 데이터들을 활용할 수 있다.
-
mutations
변이라는 뜻을 가진다. (돌연변이의 변이)
mutaions를 통해서 관리하는 데이터를 변경해줄 수 있다.
movie.js에서 관리하는 state에 있는 각각의 데이터들은 mutations로 변경하는 것을 제외하고는 getters나 actions나 다른 컴포넌트에서 데이터가 수정되는 것을 허용하지 않는다는 것을 의미한다.
즉, 오직 mutations에서만 데이터를 변경할 수 있다!
각 컴포넌트들에서 데이터를 수정할 수 있게 되면 관리가 어려워지기 때문에 mutations에 정의된 개념만을 이용하게 한 것이다.
-
actions
데이터 변경을 제외한 메소드를 작성한다.
데이터 변경은 mutations에서만 진행한다.
주의사항! actions는 비동기로 동작한다.
actions에 만들어내는 함수들은 비동기로 처리되도록 구성되어져 있다. 따라서 async, await를 작성하지 않아도 비동기로 처리가 된다!
-
about.js 파일
개발자의 정보, 사용자의 정보를 취급한다.
flex-shrink: 감소비율 없애기
의문
apply 버튼의 사이즈는 120px인데, 다른 select 사이즈와 동일함에도 더 작아보인다. 이유는 뭘까?
가로의 넓이가 정해져있기 때문에, 마지막 요소인 apply는 정해진 가로 넓이 안에 들어오기 위해서 버튼 본인의 가로 사이즈를 줄였다.
어떤 상황에서도 사이즈를 줄이지 않게 설정하려면 어떻게 할까?
해결
flex-shrink를 통해서 설정해주면 된다.
기본값은 1로, 사이즈가 유동적으로 변할 수 있도록 되어있다. 해당 값을 0으로 변경하면 어떤 상황에서도 정해둔 사이즈가 변하지 않게 된다.
axios: http 요청
npm을 통해서 axios를 설치한다.
script 상단에 import를 통해서 axios를 호출하고, apply 메소드 부분에 axios의 get을 사용하여 API Key를 요청받는다.
methods: {
apply() {
// 영화검색 기능
axios.get(`https://www.omdbapi.com/?apikey=${}&`)
}
}
추가
요청 내용을 비동기로 동작시키기 위해서 async와 await 키워드를 추가해준다.
methods: {
async apply() {
// 영화검색 기능
const OMDB_API_KEY = 'API KEY 작성'
const res = await axios.get(`https://www.omdbapi.com/?apikey=${}&`)
}
}
v-for와 v-model 동시사용
상황
v-model 디렉티브를 통해서 script에서 만들어둔 데이터인 type이나 name 등을 양방향 데이터 바인딩으로 연결해줘야 하는데 filters 배열데이터를 통해서 select를 반복적으로(v-for) 출력하고 있기 때문에 script에서 만든 데이터들을 직접적으로 명시할 수 없다.
해결
따라서, vue.js에서 제공하는 ‘$’로 시작하는 $data속성을 사용한다.
여기서 $data는 script의 data()로 보면 된다.
$data를 통해서 script의 data 안의 각 데이터들(title, type, number 등)에 직접 접근할 수 있게 된다.
($data.type
라고 하면 data의 type 데이터에 접근할 수 있게 된다.)
근데, 이 부분이 동적으로 반응을 해야 하기 때문에 $data 뒤에 대괄호를 열어서 filter의 속성(name)으로 작성하여 동적으로 사용할 수 있도록 명시한다. ($data[filter.type]
등)
<select
v-for="filter in filters"
v-model="$data[filter.name]"
:key="filter.name"
class="form-select">
<option></option>
</select>
v-if를 통해 원하는 부분에만 내용 추가
상황
년도가 출력되는 select 부분에 가장 첫번째 기본 옵션의 값으로 All Years를 추가하고자 한다.
데이터를 출력하는 option 상단에 다음과 같은 option을 추가한다.
<option
value="">
All Years
</option>
해당 옵션을 추가하면 년도 출력 부분에 처음 값으로 All Years가 표시되지만 다른 type과 number 데이터 부분에도 All Years 값이 추가되게 된다.
year 데이터를 가지는 select 부분에만 해당 값을 가지게 하려면 어떻게 할까?
해결
답은 v-if를 사용하면 된다.
v-if를 사용하여 filter의 name 값이 year인 것만 해당 option을 출력하도록 한다.
<option
v-if="filter.name == 'year'"
value="">
All Years
</option>
해당 코드를 통해 name 값이 year인 부분만 All Years 값을 가지는 것을 확인할 수 있다.