Vue.js Starter - Proxy, Vuex
Vue.js νλ‘μ νΈ ν¬μ μΌμ£ΌμΌ μ - Part 6
23. Proxy π©βπ»
1. Proxy Server
Proxy Server
λ μλ²λ₯Ό ν΅ν΄ λ€λ₯Έ λ€νΈμν¬ μλΉμ€μ κ°μ μ μΌλ‘ μ μν μ μκ² ν΄ μ£Όλ μ»΄ν¨ν° μμ€ν
μ΄λ μμ©νλ‘κ·Έλ¨μ λ§νλ€.
νλ‘μ μλ²λ λ°©λ¬Έ μ€μΈ μΉ μ¬μ΄νΈμ κΈ°κΈ° μ¬μ΄μμ μ€κ°μ μν μ νλ©°, νΈλν½μ νΈμ€νΈ μλ² μ°κ²°μ μ¬μ©λλ μ격 μμ€ν μ ν΅ν΄ μ λ¬λλ€. νλ‘μ μλ²λ₯Ό μ΄μ©νλ©΄ μ€μ IP μ£Όμλ₯Ό μ¨κΈΈ μ μμΌλ©° μΉ μ¬μ΄νΈμμλ μλ IP μ£Όμκ° μλ νλ‘μ μλ²μ IP μ£Όμλ₯Ό μΈμνκ² λλ€.
νλ‘μ μλ²μ λμ μΈ μ ν μΈ κ°μ§λ λ€μκ³Ό κ°λ€.
- HTTP Proxy : μΉνμ΄μ§μλ§ μ ν©ν νλ‘μλ‘, HTTP νλ‘μλ‘ λΈλΌμ°μ λ₯Ό μ€μ νλ©΄ λͺ¨λ λΈλΌμ°μ νΈλν½μ΄ ν΄λΉ κ²½λ‘λ₯Ό ν΅ν΄ λΌμ°ν λλ€.
- SOCKS Proxy : μΉ/μμ©νλ‘κ·Έλ¨μμ μ¬μ©ν μ μλ νλ‘μλ‘ λͺ¨λ μ’ λ₯μ νΈλν½μ μ²λ¦¬ν μ μμ§λ§ λ³΄ν΅ HTTP νλ‘μλ³΄λ€ μ°κ²° μλκ° λλ¦¬κ³ λ‘λ© μκ°μ΄ λ μ€λ κ±Έλ¦°λ€.
- Transparent Proxy : μ λ μ νκ³Όλ λ€λ₯΄κ² μ¬μ©μκ° νλ‘μλ₯Ό μ¬μ©νκ³ μλ€λ μ¬μ€ μ체λ₯Ό λͺ¨λ₯΄λ κ²½μ°κ° λλΆλΆμ΄λ€. νμ¬μμ κΈ°κΈ° μ¬μ©μμ μ¨λΌμΈ νλμ λͺ¨λν°λ§νκ±°λ νΉμ μ¬μ΄νΈ μ κ·Όμ μ°¨λ¨νλλ° μ¬μ©νκ±°λ νΈν μ΄λ μΉ΄νμμ κ³΅μ© μμ΄νμ΄μμ μ¬μ©μλ₯Ό μΈμ¦νκ³ μμΈμ€κΆνμ νμ©νλλ° μ¬μ©λλ€.
2. VPN (Virtual Private Network)
μ€μ IP λ₯Ό λΌμ°ν
μ ν΅ν΄ κ°μ IP λ‘ λμ²΄ν΄ μΉμ¬μ΄νΈμ μ μ μ 보λ₯Ό μμ΄λ κ²μ λν΄ μ΄μΌκΈ° ν λ μ£Όλ‘ ν¨κ» μΈκΈλλ κ²μ΄ λ°λ‘ VPN
μ΄λ€.
VPN μ΄ Proxy μ μ°¨μ΄μ μ μ 리νλ©΄ λ€μκ³Ό κ°λ€.
Proxy | VPN |
---|---|
κ°μ IP λ₯Ό μ¬μ© | κ°μ IP λ₯Ό μ¬μ© |
μ± μμ€μμ μλ | OS μμ€μμ μλ |
μ± νΈλν½ λΌμ°ν | λͺ¨λ νΈλν½ λΌμ°ν |
νΈλν½ μνΈν λΆκ° | νΈλν½ μνΈν |
λΉκ΅μ λΉ λ₯Έ μλ | μνΈνλ‘ μΈν΄ λ€μ λλ €μ§ |
3. CORS (Cross-Origin Resource Sharing)
μ΅μ λΈλΌμ°μ λ XSS λ± λ€μν μ·¨μ½μ μΌλ‘λΆν° λΈλΌμ°μ λ₯Ό 보νΈνκΈ° μν΄ κΈ°λ³Έμ μΌλ‘ SOP
(Same-Origin Policy) λ₯Ό κ°κ³ μλ€.
λΈλΌμ°μ μ SOP κ°
Same-Origin
μ νλ¨νλ κΈ°μ€μprotocol
,host
,port
3κ°μ§κ° λμΌνμ§λ₯Ό λΉκ΅νλ€.
http://store.company.com/dir/page.html
μ λμΌν Same-origin μΈμ§ μ¬λΆλ λ€μκ³Ό κ°λ€.
λ§μ½ Web μλ²μ API μλ²κ° λμΌ λ€νΈμν¬μ μ‘΄μ¬νκ³ , λμΌ protocol
, host
, port
λ₯Ό μ¬μ©νλλ° ν΄λΉ μλ²μ λ€νΈμν¬ λ§
κ°μ₯ μλ¨μ μ€μμΉκ° λμλ 곡μ κΈ°κ° λμλ 무μΈκ° μμ²μ λ°μ Reverse Proxy μν μ ν΄μ£Όλ λ―Έλ€μ¨μ΄ μλ²κ° μ‘΄μ¬ν΄ URL κ²½λ‘μ
μΌλΆλ₯Ό μλΌ κ΅¬λΆν΄μ£Όλβ¦ μ΄λ° μλ¦λ€μ΄ μ°μΆμ΄ μλ μ΄μ κ²°κ΅ λ μλ²λ SOP λ₯Ό μλ°ν μ λ°μ μκ² λλ€.
μ΄λ₯Ό ν΄κ²°νλ λ°©λ²μ λ κ°μ§κ° μλ€. νλλ Webpack DevServer μ Proxy
λ₯Ό μ¬μ©νλ κ²μ΄κ³ , λ€λ₯Έ νλλ CORS
μμ²μ νμ±ν νλ κ²μ΄λ€.
4. Proxy
ν΄λΌμ΄μΈνΈκ° Vue μ±μ λ°μ νμ΄μ§λ₯Ό λ λλ§ ν ν νλ‘ νΈμ κ΄λ ¨λ μμ²μ Web μλ²μ, API μμ²μ API μλ²μ λ³΄λΌ κ²μ΄λ€. λ¬Όλ‘ , Open API κ° μλ κ²½μ° λ³΄ν΅μ Web μλ²λ₯Ό ν΅ν΄ API μλ²μ μμ²μ νκ³ , API μλ²λ μΈλΆμ μ§μ λ ΈμΆμ μ μν€κΈ΄ νμ§λ§ μ΄μ¨λ SOP λ₯Ό λ§μ‘±νμ§ λͺ»ν κ²μ΄λ€.
λ§μ½ λ©μΈ API μλ²μ λν μμ²μ ν΄λΌμ΄μΈνΈκ° API μλ²μ μ§μ νλ κ²μ΄ μλκ³ Web μλ²μ Proxy μλ²λ₯Ό νλ λλ κ²μ΄λ€. κ·Έλ¬λ©΄ ν΄λΌμ΄μΈνΈλ Web μλ²νκ³ λ§ ν΅μ νκ³ , APi μλ²λ Web μλ²κ° λ§λ Proxy μλ²νκ³ λ§ ν΅μ ν κ²μ΄λ€. μ΄λ κ² λλ©΄ λΈλΌμ°μ λ Web μλ²νκ³ λ§ ν΅μ νλκ²μΌλ‘ 보μΌν λ SOP λ₯Ό μλ°νμ§ μλ κ²μΌλ‘ νλ¨νλ€.
CORS μμ΄ νλ‘ νΈμλ μ체μμ ν΄κ²°νλ λ°©λ²μΌλ‘ Proxy μλ²λ₯Ό μΆκ°ν΄ μ²λ¦¬νλ€.
- /vue.config.js
const { defineConfig } = require('@vue/cli-service')
const apiServer = 'http://localhost:3000'
module.exports = defineConfig({
transpileDependencies: true,
devServer: {
port: 8080,
proxy: {
'^/api': {
apiServer,
changeOrigin: true
}
}
}
})
Vue μλ²κ° http://localhost:8080
μ΄κ³ , API μλ²κ° http://localhost:3000
μ΄λΌ ν΄λ³΄μ. μμ κ°μ΄ Vue μ€μ μ
νλ©΄ ν΄λΌμ΄μΈνΈλ API μμ²μ http://localhost:3000/api/~~~
κ° μλ http://localhost:8080/api/~~~
λ‘
νλ©΄ λλ€. κ·Έλ¬λ©΄ Vue μλ²κ° ν΄λΉ /api
λ‘ μμνλ λͺ¨λ μμ²μ Proxy μλ²λ₯Ό μ΄μ©ν΄ http://localhost:3000
μΌλ‘
Origin μ λ³κ²½ν΄ μ€ κ²μ΄λ€.
κ·Έλ¦¬κ³ μ΄λ κ² Proxy λ₯Ό μ¬μ©νλ κ²μ μ₯μ μ€ νλλ CORS λ₯Ό μ¬μ©νμ§ μκ³ μ°νν μ μλ κ² λΏ μλλΌ νλμ μμ€ μ½λλ‘
NODE_ENV
λ₯Ό μ΄μ©ν΄ κ° νκ²½μ ꡬλΆνκ³ , νκ²½μ λ°λΌ Local
, Development Server
, Production Server
νκ²½μ
λ§κ² μλνλλ‘ ν μ μλ€λ μ μ΄λ€.
5. Access-Control-Allow-Origin
λ©μΈ API μ κ²½μ° μ΄λ κ² Proxy λ₯Ό μ΄μ©ν΄ μ체μ μΌλ‘ μ°ννλ κ²μ΄ κ°λ₯νλ€. νμ§λ§ μ±μ λ§λ€λ©΄μ SOP λ₯Ό νμ μ μ§ν μ μλ κ²½μ°λ κ±°μ μλ€. Open API λ₯Ό μ¬μ©ν μλ μκ³ , CDN μ μΊμ±λ λ°μ΄ν°λ₯Ό μ¬μ©ν μλ μλ€. μλλ©΄ Proxy μλ² μμ΄ ν΄λΌμ΄μΈνΈκ° CORS μ€μ μ ν΅ν΄ API μλ²μ μ§μ ν΅μ νλ κ²μ μν μλ μλ€.
μ΄λ νμν κ²μ΄ λΈλΌμ°μ κ° ν΄λΉ SOP λ₯Ό μλ°νλ λ€λ₯Έ μλ²λ‘λΆν° CORS νκ°λ₯Ό λ°μ λΈλΌμ°μ μ μ΄λ₯Ό μ리λ κ²μ΄λ€. κ·Έλ¬λ©΄ λΈλΌμ°μ λ SOP λ₯Ό μλ°νλ λ€λ₯Έ Origin μ κ°λ μλ²μ λν΄ μ νμ μΌλ‘ CORS ν΅μ μ νμ©ν΄μ€λ€. μ¦, Same Origin κ³Όλ SOP ν΅μ μ νκ³ , κ·Έ μΈμλ CORS ν΅μ μμλ²κ° νμ©ν λ°©μκ³Ό μκ° λμ μΊμ±μ ν΅ν΄ ν΅μ νλ€.
CORS λ₯Ό μ¬μ©ν΄ μ²λ¦¬νλ λ°©λ²μΌλ‘ λ°λμ μλ²μ μ€μ μ΄ νμνλ€.
CORS λ₯Ό νμ©νλ λ°©λ²μ ν¬κ² 2κ°μ§κ° μ‘΄μ¬νλ€.
1 ) Wildcard *
Access-Control-Allow-Origin: *
Credentials
μλ μμ²μ λν΄ *
λ₯Ό λͺ
μνλ©΄ λͺ¨λ Origin μ μ κ·Όμ νμ©νκ² λ¨ μλ―Έκ° λλ€.
2 ) Specifies an Origin
Access-Control-Allow-Origin: https://developer.mozilla.org
Vary: Origin
νΉμ Origin μ μλ΅ν κ²½μ° μλ²μ μλ΅μ΄ Origin μ λ°λΌ λ¬λΌμ§ μ μμμ μλ΅ ν€λμ λ°λμ Vary: Origin
κ³Ό ν¨κ» 보λ΄μΌ
νλ€. μ κ²½μ° λΈλΌμ°μ μ https://developer.mozilla.org
μμ²μ λν΄μλ§ μμ²ν μ μμμ μλ΅νλ€.
λ€μμ μ€μ μλ²μ μλ€.
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@Component
public class SimpleCORSFilter implements Filter {
private final Logger log = LoggerFactory.getLogger(SimpleCORSFilter.class);
public SimpleCORSFilter() {
log.info("SimpleCORSFilter init");
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // CORS λ₯Ό νμ©ν΄μ€ ν΄λΌμ΄μΈνΈμ μ£Όμ
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With, remember-me");
chain.doFilter(req, res);
}
@Override
public void init(FilterConfig filterConfig) {
}
@Override
public void destroy() {
}
}
μμ²ν ν΄λΌμ΄μΈνΈμ Origin μ κ°μ Έμμ ν΄λΉ Origin μ λν΄ CORS λ₯Ό νμ©ν¨μ μλ΅νλ€. λ€λ₯Έ μλ΅ ν€λλ μ΄ API κ° νμ©νλ CORS κ·μΉμ λν μ 보λ₯Ό λνλΈλ€.
κ·Έλ¬λ©΄ Vue μμλ axios μ€μ μ λ€μκ³Ό κ°μ΄ μΆκ°νλ€.
import axios from 'axios'
axios.defaults.baseURL = 'http://localhost:3000' // μλ² μ£Όμ
axios.defaults.headers.post['Content-Type'] = 'application/json;charset=utf-8'
axios.defaults.headers.post['Access-Control-Allow-Origin'] = '*'
6. Refactor Axios Examples with Proxy
Axios Examples with Composition API
λ₯Ό Webpack DevServer μ Proxy
λ₯Ό μ¬μ©νλλ‘ μμ ν΄λ³΄μ.
μ°μ API μμ²μ΄ Proxy λ₯Ό μ¬μ©νλλ‘ νκΈ° μν΄ Mock API μ μλ΅ λ°μ΄ν° κ²½λ‘λ₯Ό λ³κ²½ν΄μ£Όμλ€.
- /src/utils/api.js
Proxy λ₯Ό μ¬μ©ν κ²μ΄λ―λ‘ API μ baseURL μ μμ νλ€. κΈ°λ³Έκ°μΈ μλ² μ£Όμ (νμ¬μ κ²½μ°) http://localhost:8080
μ΄
κΈ°λ³Έκ°μΌλ‘ μ€μ λλ€.
import axios from 'axios'
const $api = axios.create({
})
const $get = async (url, data) =>
await $api.get(url, data).then(successHandler).catch(errorHandler);
const $post = async (url, data) =>
await $api.post(url, data).then(successHandler).catch(errorHandler);
const $put = async (url, data) =>
await $api.put(url, data).then(successHandler).catch(errorHandler);
const $patch = async (url, data) =>
await $api.patch(url, data).then(successHandler).catch(errorHandler);
const $delete = async (url, data) =>
await $api.delete(url, data).then(successHandler).catch(errorHandler);
const successHandler = (res) => {
if ((res.status / 200).toFixed() !== "1") {
throw new HTTPError(res.status, res.statusText);
} else {
return res.data;
}
};
const errorHandler = (error) => {
// Step 1. Send error to server for log.
// Step 2. Throw error to components
throw error;
};
class HTTPError extends Error {
constructor(status, statusText) {
super(`HTTP Error ${status}: ${statusText}`);
this.status = status;
this.statusText = statusText;
}
}
export { $api, $get, $post, $put, $patch, $delete };
- /vue.config.js
Vue Config μ Proxy μ€μ μ μΆκ°ν΄μ€λ€.
const { defineConfig } = require('@vue/cli-service')
const target = 'https://0000.mock.pstmn.io' // Proxy λͺ©μ μ§
module.exports = defineConfig({
transpileDependencies: true,
// Proxy μ€μ
devServer: {
port: 8080,
proxy: {
'^/api': {
target,
changeOrigin: true
}
}
}
})
- /src/views/AxiosWithCompositionAPI.vue
λ§μ§λ§μΌλ‘ API μμ² κ²½λ‘λ₯Ό Proxy κ° μΈμν μ μλλ‘ /api
λ₯Ό prefix λ‘ λ£μ΄μ£Όμ΄ Mock μλ²μ μΌμΉμν¨λ€.
<template>
<div>
<table>
<thead>
<tr>
<th>μ νλͺ
</th>
<th>κ°κ²©</th>
<th>μΉ΄ν
κ³ λ¦¬</th>
</tr>
</thead>
<tbody>
<tr v-for="(product, i) in state.productList" :key="i">
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
</div>
</template>
<script>
import Product from '@/dto/Product'
import { onMounted, reactive } from 'vue'
import { $get } from '@/utils/api'
export default {
name: 'AxiosWithCompositionAPI',
setup () {
const state = reactive({
productList: Array[Product]
})
const getList = async () => {
state.productList = await $get('/api/getProducts')
}
onMounted(() => {
getList()
})
return { state }
}
}
</script>
<style scoped>
table {
font-family: Arial, sans-serif;
border-collapse: collapse;
width: 100%;
}
td,
th {
border: 1px solid #ddd;
text-align: left;
padding: 8px;
}
</style>
Vue μλ²λ‘ μμ²νμ§λ§ μ μμ μΌλ‘ Mock μλ²μ ν΅μ ν΄ λ°μ΄ν°λ₯Ό κ°μ Έμ€λ κ²μ νμΈν μ μλ€.
24. Vuex π©βπ»
1. What is Vuex?
λ€μκ³Ό κ°μ self-contained app
μ΄ μλ€κ³ ν΄λ³΄μ.
const Counter = {
// state
data () {
return {
count: 0
}
},
// view
template: `
<div>8</div>
`,
// actions
methods: {
increment () {
this.count++
}
}
}
createApp(Counter).mount('#app')
μ΄ κ²½μ° λ°μ΄ν° νλ¦μ λ¨μνκ² one-way data flow
μ 보μΈλ€. νμ§λ§ μ΄λ€ μνλ₯Ό λ¨μΌ μ»΄ν¬λνΈκ° μλ μλ‘ λ€λ₯Έ λ·° μ»΄ν¬λνΈκ°
곡μ ν΄μΌ νλ κ²½μ°μλ μ΄λ¨κΉ?
λ μ»΄ν¬λνΈκ° λΆλͺ¨μμ μ¬μ΄μΌ κ²½μ° props
λ₯Ό μ΄μ©ν΄ 곡μ νκ³ $emit
μ ν΅ν΄ λκΈ°νκ° κ°λ₯νλ€. νμ§λ§ μ΄κ±΄ λΆλͺ¨μμ μ¬μ΄μλ§
κ°λ₯νλ€. μ¬λ¬ κ³μΈ΅μΌ κ²½μ°λ κ·Έλ§νΌ μ¬λ¬ μ°¨λ‘ drill down
ν΄μ λ΄λ €κ°μΌνλ€. λν λΆλͺ¨μμμ΄ μλ νμ μ»΄ν¬λνΈ κ°μ κ΅νμ
λΆκ°λ₯νλ€.
μ΄λ¬ν λ¬Έμ λ λ λ€λ₯Έ SPA μΈ React μμλ μ‘΄μ¬νμΌλ©°, React λ Redux λ₯Ό μ΄μ©ν΄ λ¬Έμ λ₯Ό ν΄κ²°νλ€. SPA κ° κ·λͺ¨κ° 컀μ§λ©΄μ
μ¬λ¬ μ»΄ν¬λνΈμμ μνλ₯Ό 곡μ νλ λ° μ΄λ €μμ λλΌκ²λμκ³ , μ΄λ₯Ό ν΄κ²°νκΈ° μν΄ μ¬λ¬ μ»΄ν¬λνΈμμ μνλ₯Ό 곡μ ν νμκ° μλ λ°μ΄ν°λ₯Ό
Global Sintleton
μΌλ‘μ¨ κ΄λ¦¬νλλ‘ λ³κ²½νλ€. Vue μμ μ΄ μν κ΄λ¦¬ 맀λμ μν μ νλ κ²μ΄ Vuex λΌμ΄λΈλ¬λ¦¬λ€.
Vuex λ₯Ό μ¬μ©νλ©΄ μ¬λ¬ μ»΄ν¬λνΈλ λ€μκ³Ό κ°μ΄ Vuex λ₯Ό Global Singleton
μΌλ‘ 곡μ νκ²λλ€.
2. Difference between Vuex
and Provide/Inject
Provide/Inject μμ μ¬λ¬ μ»΄ν¬λνΈμμ λ°μ΄ν°λ₯Ό μ¬μ©ν μ μμλ€. κ·Έλ λ€λ©΄ Vuex μ μ°¨μ΄κ° 무μμΌκΉ?
Provide/Inject
λ Vuex
μ μ±κ²©μ΄ λ©μ° λ€λ₯΄λ€. Provide/Inject λ³λμ λΌμ΄λΈλ¬λ¦¬κ° μλλ©°, Vue κ° μ체μ μΌλ‘ μ§μνλ
κΈ°λ₯μΌλ‘ λ°μ΄ν°μ νλ¦μ λΆλͺ¨ μ»΄ν¬λνΈμμ μμ μ»΄ν¬λνΈλ‘ νλ₯Έλ€. μ±μ μ΅μλ¨μΈ App-level
μμ μ£Όμ
νλ©΄ λͺ¨λ μ»΄ν¬λνΈμμ μ¬μ©
κ°λ₯νλλ‘ λ§λ€ μ μκΈ° λλ¬Έμ Vuex μ κ°μ΄ μ¬λ¬ μ»΄ν¬λνΈμμ μ¬μ©νλ κ²μ΄ κ°λ₯νμ§λ§, μ»΄ν¬λνΈκ° μν κ΄λ¦¬κ° λͺ©μ μ΄ μλλ€.
- Provide/Inject : λΆλͺ¨μμ μμμΌλ‘ νΈλ¦¬ κ΅¬μ‘°κ° 1 κ³μΈ΅ μ΄μμΈ κ²½μ° μμ½κ² drill down νκΈ° μν΄ μ¬μ©νλ€.
- Vuex : μ¬λ¬ μ»΄ν¬λνΈμμ μνλ₯Ό 곡μ νκΈ° μν΄ μ¬μ©νλ€.
μ¬μ€ λ€λ₯Έ μ± κ°λ°μμ Reference Types μΈ
Class
μ μκΈ° μμ μ static λ³μλ‘ λ§λ€κ³ Initializer λ₯Ό private μΌλ‘ λ§λ€μ΄Singleton Design Pattern
μ μ μ©νλ©΄ μμ½κ² ν΄κ²°λλ λ¬Έμ μ΄λ€. μλλ©΄ λ λ¨μν λ°μ΄ν° μ μ₯ 곡μ κ° λͺ©μ μ΄λ©΄Type Properties
μType Methods
λ₯Ό μ¬μ©νλ©΄ λλ κ°λ¨ν λ¬Έμ μ§λ§ Vue λ κ²°κ΅ λΈλΌμ°μ νκ²½μμ, νλμ Document μμμ μ΄λ£¨μ΄μ ΈμΌνλ SPA νΉμ±μ νκ³λ₯Ό 극볡νκΈ° μν΄ μ겨λ λΌμ΄λΈλ¬λ¦¬μΈ κ²μ΄λ€.
3. Next Generation is Pinia
Vuex 3.x λ Vue 2 λ₯Ό μνκ²μ΄μκ³ , Vuex 4.x λ Vue 3 λ₯Ό μν κ²μ΄μλ€. κ·Έλ¦¬κ³ Vuex μ λ€μ λ²μ μΈ Vuex 5 μ λν΄ μ¬λ¬
μμ΄λμ΄λ₯Ό ν λ‘ νλ μ€ μ΄λ―Έ Vuex 5 μμ μνλ λλΆλΆμ ꡬννκ³ μλ Piania
κ° μ΄λ―Έ μ‘΄μ¬νκ³ μλ€λ κ²μ μκ² λμλ€. λ°λΌμ Vuex 5
λ₯Ό κ°λ°νλ λμ Vue μ 곡μ μν κ΄λ¦¬ λΌμ΄λΈλ¬λ¦¬λ Pinia
λ‘ λ³κ²½λμμΌλ©°, Vuex 3 κ³Ό 4λ κ³μ μ μ§λ λμ§λ§ κΈ°λ₯ μΆκ°κ° λμ§λ
μμ κ²μ΄λΌ νλ€.
μ¬μ€μ λμ μ
κ·Έλ μ΄λ λ²μ μ΄ μλ λ€λ₯Έ λΌμ΄λΈλ¬λ¦¬μ΄λ―λ‘ νλμ νλ‘μ νΈ λ΄μ Pinia
μ Vuex
λ₯Ό λͺ¨λ μ€μΉνλ κ²μ΄ κ°λ₯νλ€.
μ΄λ‘μ¨ κΈ°μ‘΄μ Vuex λ₯Ό μ¬μ©μ€μΈ μ±μ΄ Pinia λ‘ λ§μ΄κ·Έλ μ΄μ
νλ κ²μ μ μ§μ μΌλ‘ μ²λ¦¬ν μ μμ κ²μ΄λ€. κ·Έλ¬λ μ νλ‘μ νΈλ₯Ό μμν
κ³νμ΄λΌλ©΄ λ μ΄μ Vuex λ₯Ό μ¬μ©νμ§ λ§κ³ Pinia
λ₯Ό μ¬μ©ν κ²μ κΆμ₯νκ³ μλ€.
Comparison with Vuex 3.x/4.x ΒΆ
- Pinia μλ λ μ΄μ
mutations
κ° μ‘΄μ¬νμ§ μλλ€. - TypeScript μ Type Inference λ₯Ό νμ©νλ―λ‘ λ μ΄μ TypeScript λ₯Ό μ§μνκΈ° μν΄ Custom Complex Wrappers λ₯Ό λ§λ€ νμκ° μλ€.
- μλμμ±μ μ§μνλ€.
- λ μ΄μ λμ μΌλ‘
store
λ₯Ό μΆκ°ν νμκ° μλ€. Pinia μμλ μ΄λ―Έ λμ μ΄λ€. λ§μ½ μ§μ λ€λ£¨κΈΈ μνλ€λ©΄ ν μλ μμ§λ§ μ¬μ©μκ° λμΉμ±μ§ λͺ»νλλΌλ μ΄λ―Έ λμ μΌλ‘ κ΄λ¦¬λλλ‘ μλν λμ΄ μμΌλ―λ‘ κ·Έλ΄ νμκ° μλ€. - λ μ΄μ μ€μ²©λ λͺ¨λ κ΅¬μ‘°κ° μλ€. μ¬μ ν store λ₯Ό λ€λ₯Έ store μμμ import ν¨μΌλ‘μ¨ nest store λ₯Ό μμμ μΌλ‘ ν¬ν¨ν μ μμ§λ§ , Pinia λ νλ©΄ κ΅¬μ‘°λ‘ μ΄λ₯Ό λμμΈ ν΄ μ 곡νλ λμμ stores κ°μ κ΅μ°¨ ꡬμ±μ κ°λ₯νκ² νλ€(Stores μ μν μ’ μμ κ°μ§ μλ μλ€).
- λ μ΄μ
Namespaced Modules
κ° μ‘΄μ¬νμ§ μλλ€. Stores κ°flat architecture
λ‘ μ 곡λλ―λ‘namespacing
μ μ΄λ»κ² μ μλμλκ°μ μν΄ μμλλ―λ‘ λͺ¨λ stores λnamedspaced
λμλ€ ν μ μλ€.
4. Installation
Pinia λ λμ€μ λ€μ μμ보λλ‘νκ³ μ± μ λ΄μ©μ λ§μΆ° Vuex λ₯Ό μ΄μ©ν΄ μ§ννλλ‘ νλ€.
npm install vuex@next --S
5. Store with Options API
Vuex λ Pinia λ λͺ¨λ Vue μ State Management Library
λ€. κ·Έλ¦¬κ³ μ΄λ€μ΄ μ μ₯νλ λ°μ΄ν°λ Store
λΌ λΆλ₯΄λ Object
컨ν
μ΄λμ μ μ₯λμ΄ κ΄λ¦¬λλ€.
- /src/store/index.js
import { createStore } from 'vuex'
export default createStore({
state () {
return {
count: 0
}
},
mutations: {
increment (state) {
state.count++
}
// μλμ κ°μ΄ this.state λ‘λ μ κ·Ό κ°λ₯νλ€. νμ§λ§ Vuex 곡μ μμ λ₯Ό 보면 μμ κ°μ λ°©λ²μ μ¬μ©νλλ‘ μ§μνλ€.
// increment () {
// this.state.count++
// }
}
})
- state : 곡μ λ λ°μ΄ν° Object λ₯Ό μ μνλ€.
- mutations : state λ μΈλΆμμ μ κ·Όν΄ λ³κ²½νλ κ²μ κ°λ₯νλ, λ΄λΆμμλ mutations λ₯Ό ν΅ν΄μλ§ λ³κ²½λλ€. μ΄ mutations λ₯Ό κ° μ»΄ν¬λνΈμμ νΈμΆν λλ
this.$store.commit('mutation-method-name')
μ ν΅ν΄ νΈμΆνλ€.
- /src/views/StoreOptionsAPI.vue
<template>
<p>Count: {{ count }}</p>
<button type="button" @click="increment">Increment</button>
</template>
<script>
export default {
name: 'StoreOptionsAPI',
computed: {
count () {
return this.$store.state.count
}
},
methods: {
increment () {
// this.$store.state.count++
this.$store.commit('increment')
}
}
}
</script>
<style scoped>
p { color: red; }
</style>
mutations λ₯Ό μ¬μ©νμ§ μκ³ μΈλΆμμ μ§μ state μ κ°μ λ³κ²½νλ κ²μ΄ κ°λ₯νλ€(i.e.
this.$store.state.count++
).
μ΄κ²μ΄ κ°λ₯ν κ²μΌλ‘ 보μ member κ° private μΌλ‘ κ΄λ¦¬λμ λ΄λΆμμ λ³κ²½νλλ‘ setter λ₯Ό μ¬μ©νλ― mutations methods λ₯Ό μ¬μ©ν μ λ°μ μλ κ²μ μλ κ² κ°λ€. λ€μμactions
λ₯Ό μ€λͺ νλ©΄μ λ€μ μ΄μΌκΈ° νκ² μ§λ§ μλ§mutations
κ°Synchronous
λ‘ μλνκΈ° λλ¬ΈμΈ κ² κ°λ€. κ·Έλ κΈ° λλ¬Έμ Vuex 곡μ λ¬Έμλ₯Ό 보면 Vuex λ μν λ³νμ mutations λ₯Ό μ¬μ©ν΄ μνλ₯Ό λ³κ²½νλλ‘ μλ΄νλ κ²μΌλ‘ 보μΈλ€. λν 곡μ λ¬Έμμ mutations νλͺ©μ 보면 devTool μ΄ μν λ³νλ₯Ό μ€λ μ·μ μ΄μ©ν΄ μΆμ ν μ μλ€κ³ νλ€.Vuex 컨ν μ΄λ μ체λ Reference Types μ Singleton μ λͺ¨λΈλ‘ νκ³ μλ€λ κ²λ§ μ μΈνλ©΄ Vuex μ member μ μ±μ§μ Swift μμ Structures μ μλκ³Ό μ μ¬ν΄λ³΄μΈλ€.
νμ§λ§, Vuex λ μ΄μ deprecated λ κ²μ΄λ λ§μ°¬κ°μ§κ³ Pinia λ mutations κ° μμΌλ μΆν migration μ μν΄μλΌλ mutations λ₯Ό μ¬μ©νλ κ²μ μ§μνλ κ²μ΄ μ’μ κ² κ°λ€.
- /src/views/AnotherStoreOptionsAPI.vue
<template>
<p>Count: {{ count }}</p>
<button type="button" @click="increment">Increment</button>
</template>
<script>
export default {
name: 'AnotherStoreOptionsAPI',
computed: {
count () {
return this.$store.state.count
}
},
methods: {
increment () {
this.$store.commit('increment')
}
}
}
</script>
<style scoped>
p { color: blue; }
</style>
Vuex Store 1
νμ΄μ§μμ λ³κ²½ν μνκ°μ΄ Vuex Store 2
νμ΄μ§μμλ μ μ§λλ κ²μ νμΈν μ μλ€.
6. Store with Composition API
μμμ 2κ°μ Vuex νμ΄μ§λ λͺ¨λ Options API λ₯Ό μ¬μ©νλ€. Composition API
μμ Vuex λ₯Ό μ¬μ©νλλ‘ λ³κ²½ν΄λ³΄μ.
computed
, reactive
, ref
, toRefs
λ±μ μ¬μ©νκΈ° μν΄ vue
μμ κΈ°λ₯μ import νλ κ²μ²λΌ
vuex
λ‘λΆν° useStore
λ₯Ό import ν΄μ μ¬μ©νλ€.
AnotherStoreOptionsAPI
λ₯Ό StoreCompositionAPI
λ‘ λ°κΎΈκ³ λ€μκ³Ό κ°μ΄ μ½λλ₯Ό μμ νλ€.
- /src/views/StoreCompositionAPI.vue
<template>
<p>Count: {{ count }}</p>
<button type="button" @click="increment">Increment</button>
</template>
<script>
import { useStore } from 'vuex'
import { computed } from 'vue'
export default {
name: 'StoreCompositionAPI',
setup () {
const store = useStore()
return {
count: computed(() => store.state.count),
increment: () => store.commit('increment')
}
}
}
</script>
<style scoped>
p { color: blue; }
</style>
κΈ°λ₯μ΄ μ μ λλ μμ κ°μ΄ μμ±ν΄λ 무방νμ§λ§ μ»΄ν¬λνΈμ κΈ°λ₯μ΄ λ§μ κ²½μ° λΆμνκΈ° μ΄λ €μμ§ μ μλ€. vuex μ counter μ μ°κ΄λ κΈ°λ₯μ λΆλ¦¬μμΌλ³΄μ.
<template>
<p>Count: {{ count }}</p>
<button type="button" @click="increment">Increment</button>
</template>
<script>
import { useStore } from 'vuex'
import { computed, reactive, toRefs } from 'vue'
const storeCounter = (store) => {
const state = reactive({
count: computed(() => store.state.count),
increment: () => store.commit('increment')
})
return toRefs(state)
}
export default {
name: 'StoreCompositionAPI',
setup () {
const store = useStore()
const { count, increment } = storeCounter(store)
return { count, increment }
}
}
</script>
<style scoped>
p { color: blue; }
</style>
7. Getters
μμμλ λ¨μν Store μ μ μ₯λ state μ κ°μ κ°μ Έμ€κΈ°λ§νλ€. κ·Έλ°λ° λ§μ½ κ°μ κ°μ Έμ€κΈ° μν custom λ‘μ§(μλ₯Ό λ€μ΄ filter λ₯Ό μννκ±°λ, prefix λ₯Ό λΆμ΄κ±°λ, 2κ°μ κ°μ κ²°ν©νλ λ±)μ΄ νμν κ²½μ° λͺ¨λ μ»΄ν¬λνΈμμ μ΄λ₯Ό copy/paste ν΄μΌνλ€. λ°λΌμ μ΄λ° λ‘μ§μ ν΄λΉ κ°μ²΄ λ΄μμ λ‘μ§μ μννκ³ κ·Έ κ²°κ³Όλ₯Ό κ°μ Έλ€ μ¬μ©ν μ μλλ‘ μΊ‘μν νλ κ²μ΄ μ’λ€.
increment κ° λͺ λ² νΈμΆλμλμ§λ₯Ό getter λ₯Ό μ΄μ©ν΄ κ°μ Έμ보μ.
- /src/store/index.js
import { createStore } from 'vuex'
export default createStore({
state () {
return {
count: 0
}
},
mutations: {
increment (state) {
state.count++
}
},
getters: {
calledEvenTimes (state) {
return state.count % 2 === 0
}
}
})
getters μμ λ‘μ§μ μ΄λ―Έ μ μ©νκΈ° λλ¬Έμ λͺ¨λ μ»΄ν¬λνΈκ° λμΌν κ²°κ³Όλ₯Ό μ»μ μ μλ€. λ¨μ§ μ»΄ν¬λνΈλ μ‘°νλ§ νλ©΄ λλ€.
- /src/views/StoreOptionsAPI.vue
<template>
<p>
Count: {{ count }}
<span>{{ calledEvenTimes ? 'μ§μλ² νΈμΆλμμ΅λλ€' : 'νμλ² νΈμΆλμμ΅λλ€' }}</span>
</p>
<button type="button" @click="increment">Increment</button>
</template>
<script>
export default {
name: 'StoreOptionsAPI',
computed: {
count () {
return this.$store.state.count
},
calledEvenTimes () {
return this.$store.getters.calledEvenTimes
}
},
methods: {
increment () {
this.$store.commit('increment')
}
}
}
</script>
<style scoped>
p { color: red; }
</style>
- /src/views/StoreCompositionAPI.vue
<template>
<p>
Count: {{ count }}
<span>{{ calledEvenTimes ? 'μ§μλ² νΈμΆλμμ΅λλ€' : 'νμλ² νΈμΆλμμ΅λλ€' }}</span>
</p>
<button type="button" @click="increment">Increment</button>
</template>
<script>
import { useStore } from 'vuex'
import { computed, reactive, toRefs } from 'vue'
const storeCounter = (store) => {
const state = reactive({
count: computed(() => store.state.count),
calledEvenTimes: computed(() => store.getters.calledEvenTimes),
increment: () => store.commit('increment')
})
return toRefs(state)
}
export default {
name: 'StoreCompositionAPI',
setup () {
const store = useStore()
const { count, calledEvenTimes, increment } = storeCounter(store)
return { count, calledEvenTimes, increment }
}
}
</script>
<style scoped>
p { color: blue; }
</style>
Vue 3.0 λ²μ μμ Getters μ κ²°κ³Όκ° Computed Properties μ μν΄ μΊμ± λμ§ μλ λ¬Έμ κ° λ³΄κ³ λμλ€. ν΄λΉ λ¬Έμ λ₯Ό μ κΈ°ν κΉμ PR μ 보면 Vue 3.1 λΆν° μ μ μλνλ κ²μΌλ‘ 보μΈλ€.
8. Mutations and Actions
Pinia λ mutations κ° μ‘΄μ¬νμ§ μμΌλ―λ‘ λ μ΄μ νμν κ°λ μ μλλ€. μ νν κ²μ Pinia μμ μν κ΄λ¦¬λ₯Ό μ΄λ»κ² νκ³ κ°μ λ€λ£¨λμ§λ₯Ό νμΈν ν ν¬μ€ν μ μΌλΆ μμ ν νμκ° μμ κ²μΌλ‘ 보μΈλ€. λ¨, μμ§ Vuex λ₯Ό μ¬μ©μ€μ΄λΌλ©΄ mutations λ₯Ό μ¬μ©νκ³ μμν λ° Vuex μμ mutations μμ΄λ μ»΄ν¬λνΈμμ state μμ μ΄ κ°λ₯νμμλ λΆκ΅¬νκ³ mutations λ₯Ό μ¬μ©ν μ΄μ λ Vuex μ Actions μ€λͺ μ 보면 λ€μμΌλ‘ μΆμΈ‘λλ€.
- Mutations : Synchronous λ‘ μλ.
- Actions : Asynchronous λ‘ μλνλ©° μ¬λ¬ κ°μ mutations λ₯Ό μ€νν μ μλ€.
Action handlers λ Store instance μ λμΌν context
object λ₯Ό λ
ΈμΆμν΄μΌλ‘μ¨ μλνλ©° Store μ 4κ°μ§ κΈ°λ₯μ μ¬μ©ν
μ μλ€.
- context.state : state μ μ κ·Όνλ€.
- context.getter : getter μ μ κ·Όνλ€.
- context.commit : mutation μ commit νλ€.
- context.dispatch : action μ νΈμΆνλ€.
context.state λ store.state μ κ°κ³ , context.dispatch λ store.dispatch μ κ°λ€. context arguments λ‘ λ£μ§ μκ³ κ·Έλ₯ store λ₯Ό μ¨λ λλ€.
Actions λ₯Ό μ΄μ©ν΄ Mutations μ increment λ₯Ό νΈμΆνλλ‘ λ³κ²½ν΄λ³΄μ.
- /src/store/index.js
import { createStore } from 'vuex'
export default createStore({
state () {
return {
count: 0
}
},
mutations: {
increment (state) {
state.count++
}
},
getters: {
calledEvenTimes (state) {
return state.count % 2 === 0
}
},
actions: {
incrementInActions (context) {
context.commit('increment')
}
}
})
κ·Έλ¦¬κ³ context.commit
, store.commit
μ κ·Έλ₯ λ€μκ³Ό κ°μ΄ μΆμ½ν΄μ μ¬μ©ν μ μλ€.
import { createStore } from 'vuex'
export default createStore({
// ...
actions: {
incrementInActions ({ commit }) {
console.log('\'increment\' will be called by actions.')
commit('increment')
}
}
})
- /src/views/StoreOptionsAPI.vue
<template>
<p>
Count: {{ count }}
<span>{{ calledEvenTimes ? 'μ§μλ² νΈμΆλμμ΅λλ€' : 'νμλ² νΈμΆλμμ΅λλ€' }}</span>
</p>
<button type="button" @click="increment">Increment</button><br><br>
<button type="button" @click="incrementInActions">Increment(called by actions)</button>
</template>
<script>
export default {
name: 'StoreOptionsAPI',
computed: {
count () {
return this.$store.state.count
},
calledEvenTimes () {
return this.$store.getters.calledEvenTimes
}
},
methods: {
increment () {
this.$store.commit('increment')
},
incrementInActions () {
this.$store.dispatch('incrementInActions')
}
}
}
</script>
<style scoped>
p { color: red; }
</style>
mutations λ
commit
μΌλ‘ νΈμΆνκ³ , actions λdispatch
λ‘ νΈμΆνλ€λ κ²μ κΌ κΈ°μ΅νμ.
- /src/views/StoreCompositionAPI.vue
<template>
<p>
Count: {{ count }}
<span>{{ calledEvenTimes ? 'μ§μλ² νΈμΆλμμ΅λλ€' : 'νμλ² νΈμΆλμμ΅λλ€' }}</span>
</p>
<button type="button" @click="increment">Increment</button><br><br>
<button type="button" @click="incrementInActions">Increment(called by actions)</button>
</template>
<script>
import { useStore } from 'vuex'
import { computed, reactive, toRefs } from 'vue'
const storeCounter = (store) => {
const state = reactive({
count: computed(() => store.state.count),
calledEvenTimes: computed(() => store.getters.calledEvenTimes),
increment: () => store.commit('increment'),
incrementInActions: () => store.dispatch('incrementInActions')
})
return toRefs(state)
}
export default {
name: 'StoreCompositionAPI',
setup () {
const store = useStore()
const { count, calledEvenTimes, increment, incrementInActions } = storeCounter(store)
return { count, calledEvenTimes, increment, incrementInActions }
}
}
</script>
<style scoped>
p { color: blue; }
</style>
κ·Όλ° λ§μ mutations μ increment λ₯Ό
setTimeout(() => state.count++, 3000)
λ‘ λ°κΏ ν μ€νΈ ν΄λ³΄λ Mutations μμλ setTimout μ΄ λ¬Έμ μμ΄ μλνλ€. κ²°κ΅ μ±κΈ μ€λ λμΈ JavaScript μμ λμ μ°¨μ΄κ° μλ κ² μλκ° μκ°λλλ° μ΄ λΆλΆμ μ’ λ κΉκ² μ΄ν΄λ΄μΌ ν κ²μΌλ‘ 보μΈλ€.
9. Account Examples
Vuex κ° μ€λ¬΄μμ κ°μ₯ λ§μ΄ μ¬μ©λλ μλ μ¬μ©μκ° λ‘κ·ΈμΈ νμ λ κ·Έ μ¬μ©μ μ 보λ₯Ό μ μ₯νλ κ²μ΄λ€.
import { createStore } from 'vuex'
import persistedstate from 'vuex-persistedstate'
const store = createStore({
state () {
return {
user: { }
}
},
mutations: {
user (state, data) {
state.user = data
}
},
plugins: [
persistedstate({
path: ['user']
})
]
})
export default store
Reference
- κ³ μΉμ. Vue.js νλ‘μ νΈ ν¬μ μΌμ£ΌμΌ μ . λΉμ μ΄νΌλΈλ¦ Chapter 10, 2021.
- κ³ μΉμ. Vue.js νλ‘μ νΈ ν¬μ μΌμ£ΌμΌ μ . λΉμ μ΄νΌλΈλ¦ Chapter 11, 2021.
- AgnΔ AugustΔnΔ. βνλ‘μ VPN, μλ‘ μ΄λ»κ² λ€λ₯ΌκΉ?.β NordVPN. last modified Dec. 12, 2021, VPN vs. Proxy.
- βSame-origin policy.β MDN Web Docs. Dec. 13, 2022, accessed Jan. 22, 2023, MDN - Same-origin policy.
- βAccess-Control-Allow-Origin.β MND Web Docs. Jan. 07, 2023, accessed Jan. 22, 2023, MDN - Access-Control-Allow-Origin.
- βWhat is Vuex.β Vuex Docs. Oct. 15, 2022, accessed Jan. 23, 2023, Vuex Documentation.
- βWhat is Pinia.β Pinia Docs. accessed Jan. 24, 2024, Pinia Documentation.