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이닀.

Proxy vs. VPN

VPN 이 Proxy 의 차이점을 μ •λ¦¬ν•˜λ©΄ λ‹€μŒκ³Ό κ°™λ‹€.

Proxy VPN
가상 IP λ₯Ό μ‚¬μš© 가상 IP λ₯Ό μ‚¬μš©
μ•± μˆ˜μ€€μ—μ„œ μž‘λ™ OS μˆ˜μ€€μ—μ„œ μž‘λ™
μ•± νŠΈλž˜ν”½ λΌμš°νŒ… λͺ¨λ“  νŠΈλž˜ν”½ λΌμš°νŒ…
νŠΈλž˜ν”½ μ•”ν˜Έν™” λΆˆκ°€ νŠΈλž˜ν”½ μ•”ν˜Έν™”
비ꡐ적 λΉ λ₯Έ 속도 μ•”ν˜Έν™”λ‘œ 인해 λ‹€μ†Œ 느렀짐

3. CORS (Cross-Origin Resource Sharing)

μ΅œμ‹  λΈŒλΌμš°μ €λŠ” XSS λ“± λ‹€μ–‘ν•œ μ·¨μ•½μ μœΌλ‘œλΆ€ν„° λΈŒλΌμš°μ €λ₯Ό λ³΄ν˜Έν•˜κΈ° μœ„ν•΄ 기본적으둜 SOP(Same-Origin Policy) λ₯Ό κ°–κ³  μžˆλ‹€.

Same-origin requests and Cross-origin requests

λΈŒλΌμš°μ €μ˜ SOP κ°€ Same-Origin을 νŒλ‹¨ν•˜λŠ” 기쀀은 protocol, host, port 3가지가 λ™μΌν•œμ§€λ₯Ό λΉ„κ΅ν•œλ‹€.

http://store.company.com/dir/page.html 와 λ™μΌν•œ Same-origin 인지 μ—¬λΆ€λŠ” λ‹€μŒκ³Ό κ°™λ‹€.

Same-origin Policy Examples

λ§Œμ•½ 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 의 응닡 데이터 경둜λ₯Ό λ³€κ²½ν•΄μ£Όμ—ˆλ‹€.

Mock API with Proxy

  • /src/utils/api.js

Proxy λ₯Ό μ‚¬μš©ν•  κ²ƒμ΄λ―€λ‘œ API 의 baseURL 을 μ‚­μ œν•œλ‹€. 기본값인 μ„œλ²„ μ£Όμ†Œ (ν˜„μž¬μ˜ 경우) http://localhost:8080 이 κΈ°λ³Έκ°’μœΌλ‘œ μ„€μ •λœλ‹€.

Change of API

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 섀정을 μΆ”κ°€ν•΄μ€€λ‹€.

Change of Vue Config

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 μ„œλ²„μ™€ μΌμΉ˜μ‹œν‚¨λ‹€.

Change of View requests

<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>


Proxy Result

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')

Vue Data Flow

이 경우 데이터 흐름은 λ‹¨μˆœν•˜κ²Œ one-way data flow을 보인닀. ν•˜μ§€λ§Œ μ–΄λ–€ μƒνƒœλ₯Ό 단일 μ»΄ν¬λ„ŒνŠΈκ°€ μ•„λ‹Œ μ„œλ‘œ λ‹€λ₯Έ λ·° μ»΄ν¬λ„ŒνŠΈκ°€ κ³΅μœ ν•΄μ•Ό ν•˜λŠ” κ²½μš°μ—λŠ” μ–΄λ–¨κΉŒ?

두 μ»΄ν¬λ„ŒνŠΈκ°€ λΆ€λͺ¨μžμ‹ 사이일 경우 propsλ₯Ό μ΄μš©ν•΄ κ³΅μœ ν•˜κ³  $emit을 톡해 동기화가 κ°€λŠ₯ν•˜λ‹€. ν•˜μ§€λ§Œ 이건 λΆ€λͺ¨μžμ‹ μ‚¬μ΄μ—λ§Œ κ°€λŠ₯ν•˜λ‹€. μ—¬λŸ¬ 계측일 κ²½μš°λŠ” 그만큼 μ—¬λŸ¬ μ°¨λ‘€ drill down ν•΄μ„œ λ‚΄λ €κ°€μ•Όν•œλ‹€. λ˜ν•œ λΆ€λͺ¨μžμ‹μ΄ μ•„λ‹Œ ν˜•μ œ μ»΄ν¬λ„ŒνŠΈ 간에 κ΅ν™˜μ€ λΆˆκ°€λŠ₯ν•˜λ‹€.

μ΄λŸ¬ν•œ λ¬Έμ œλŠ” 또 λ‹€λ₯Έ SPA 인 React μ—μ„œλ„ μ‘΄μž¬ν–ˆμœΌλ©°, React λŠ” Redux λ₯Ό μ΄μš©ν•΄ 문제λ₯Ό ν•΄κ²°ν–ˆλ‹€. SPA κ°€ 규λͺ¨κ°€ μ»€μ§€λ©΄μ„œ μ—¬λŸ¬ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μƒνƒœλ₯Ό κ³΅μœ ν•˜λŠ” 데 어렀움을 λŠλΌκ²Œλ˜μ—ˆκ³ , 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ μ—¬λŸ¬ μ»΄ν¬λ„ŒνŠΈμ—μ„œ μƒνƒœλ₯Ό κ³΅μœ ν•  ν•„μš”κ°€ μžˆλŠ” 데이터λ₯Ό Global Sintleton으둜써 κ΄€λ¦¬ν•˜λ„λ‘ λ³€κ²½ν–ˆλ‹€. Vue μ—μ„œ 이 μƒνƒœ 관리 λ§€λ‹ˆμ € 역할을 ν•˜λŠ” 것이 Vuex λΌμ΄λΈŒλŸ¬λ¦¬λ‹€.

Vuex λ₯Ό μ‚¬μš©ν•˜λ©΄ μ—¬λŸ¬ μ»΄ν¬λ„ŒνŠΈλŠ” λ‹€μŒκ³Ό 같이 Vuex λ₯Ό Global Singleton으둜 κ³΅μœ ν•˜κ²Œλœλ‹€.

Vuex Data Flow

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 μ»¨ν…Œμ΄λ„ˆμ— μ €μž₯λ˜μ–΄ κ΄€λ¦¬λœλ‹€.

Store Tree

  • /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 State 1

Vuex State 2

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

  1. κ³ μŠΉμ›. Vue.js ν”„λ‘œμ νŠΈ νˆ¬μž… 일주일 μ „. λΉ„μ œμ΄νΌλΈ”λ¦­ Chapter 10, 2021.
  2. κ³ μŠΉμ›. Vue.js ν”„λ‘œμ νŠΈ νˆ¬μž… 일주일 μ „. λΉ„μ œμ΄νΌλΈ”λ¦­ Chapter 11, 2021.
  3. AgnΔ— AugustΔ—nΔ—. β€œν”„λ‘μ‹œ VPN, μ„œλ‘œ μ–΄λ–»κ²Œ λ‹€λ₯ΌκΉŒ?.” NordVPN. last modified Dec. 12, 2021, VPN vs. Proxy.
  4. β€œSame-origin policy.” MDN Web Docs. Dec. 13, 2022, accessed Jan. 22, 2023, MDN - Same-origin policy.
  5. β€œAccess-Control-Allow-Origin.” MND Web Docs. Jan. 07, 2023, accessed Jan. 22, 2023, MDN - Access-Control-Allow-Origin.
  6. β€œWhat is Vuex.” Vuex Docs. Oct. 15, 2022, accessed Jan. 23, 2023, Vuex Documentation.
  7. β€œWhat is Pinia.” Pinia Docs. accessed Jan. 24, 2024, Pinia Documentation.