Realtime broadcasting with Laravel and NuxtJS (3/5)

A chat application using Laravel Echo server?

If you follow along, you’d know that making a chat application using Echo server is possible but, not quite ideal ’cause it’s not what it’s built for. It’s more of a general purpose socket.io server with some Laravel specific goodies like user authentication for private channel and notification channel scaffolding.

When you look into the Echo server’s “out of the box” event publishing feature, there’re 2 different ways of doing it.

First off, Redis PUBSUB using psubscribe() API

If you want to send an event this way, you need to broadcast(new LaravelEvent) on a Laravel server to publish it to the Redis channel specified in the LaravelEvent. You see it constantly involves 4 hop relays. 1) A Nuxt app calls Laravel API, 2) Laravel API dispatches the broadcast event via a queue, 3) Laravel worker retrieves it, publish it to Redis using Laravel’s default Redis configuration, 4) Redis client on Echo server receives it and Socket.io client on the same server handles the payload accordingly. You get the picture.

Secondly, HTTP call to Echo server

Like the following JS snippet, all it takes is an HTTP call and Socket.io client on Echo server handles it. That’s it. It is way simpler than the previous one as we don’t need the communication layers in the middle. Nice…

  async chat({ commit }, params) {
    const appId = 'bbbbccccddddeeee'
    const authKey = '33334444555566667777888899990000'
    const socketId = this.$echo.socketId()
    const roomId = params.room
    const adapter = this.$axios.create({
      baseURL: `http://localhost:6001`,
      headers: {
        common: {
          Authorization: authKey,
          Accept: 'application/json;charset=UTF-8',
          'Content-Type': 'application/json',
          'X-Socket-ID': socketId
        }
      }
    })

    await adapter.$post(`/apps/${appId}/events`, {
      channel: `private-chat.${roomId}`,
      name: 'App\\Events\\ChatMessageSent',
      data: {
        room: roomId,
        payload: params.payload
      },
      socket_id: socketId
    })
  },

I’m Sold.

Things that you have to go through to make use of MongoDB on AWS isn’t as smooth as you might think. So I’d rather settle down with the following snippet inside of docker-compose.yml within my local Laradock folder to play with DynamoDB this time. Even though I prefer MongoDB over DynamoDB, taking care of any kinda infra-structure myself would be a pain in the ass. The admin panel (https://github.com/aaronshaf/dynamodb-admin) that I’ve already installed on my 🍎Mac seems fine too.

### AWS DynamoDB Local #####################################
    dynamodb-local:
      image: amazon/dynamodb-local
      command: -jar DynamoDBLocal.jar -dbPath /var/opt/dynamodb/data -sharedDb
      volumes:
        - ${DATA_PATH_HOST}/dynamodb/data:/var/opt/dynamodb/data
      ports:
        - "8000:8000"
      networks:
        - backend

Now what?

Hold on, I could completely ditch the Echo or, partially tweak it to get the most out of it. I’ll have to take a close look at how things work in Echo server first. A couple of things that I want to address with it;

  • An efficient chat server preferably built on top of Laravel Echo server
  • All the bells and whistles of modern chat app features
    • user online status
    • previous message history (save/load data)
    • read flag of each message (read cursors?)
    • unread message count
    • user notification hook (if he/she misses it)
    • horizontal scalability

Posted by admin in Backend, 0 comments

Realtime broadcasting with Laravel and NuxtJS (2/5)

Local Setup

일단, Laravel 6.16 와 NuxtJS 2.11 λ₯Ό μ‚¬μš©ν•œ “out of the box” Laravel λΈŒλ‘œλ“œμΊμŠ€νŠΈ κΈ°λŠ₯이 둜컬 ν™˜κ²½μ—μ„œ μž‘λ™ν•˜λ„λ‘ μ„€μ • ν›„, μ „λ°˜μ μΈ κ΅¬ν˜„ λ””ν…ŒμΌμ„ 뢄석해 λ³Έλ‹€.

μ‚¬μš©μžκ°€ λΈŒλΌμš°μ €λ‘œ 접속할 frontend μ£Όμ†ŒλŠ” localhost:3000 이고, Laradock 으둜 μ„€μ •ν•œ backend API λŠ” amuse.test λΌλŠ” μ£Όμ†Œ 값을 κ°–κ³  있으며, Echo μ„œλ²„λŠ” websockets.test:6001 둜 μ ‘κ·Όκ°€λŠ₯ν•œ ꡬ성이닀. μ•„λž˜μ˜ 그림을 μ°Έκ³ ν•˜κΈ° λ°”λž€λ‹€. Laradock ν™˜κ²½μ—μ„œ, Echo μ„œλ²„κ°€ amuse.test 으둜, μ ‘κ·Όν•  수 μžˆλ„λ‘ μ„€μ •ν•˜λŠ” 것이 tricky ν•  수 μžˆλŠ”λ° μ΄λŠ” 이전 Laradock κ΄€λ ¨ ν¬μŠ€νŒ…μ„ μ°Έκ³ ν•˜κΈ° λ°”λž€λ‹€. (https://whatsupkorea.com/2018/05/26/laradock-with-muliple-projects)

Laravel λΈŒλ‘œλ“œμΊμŠ€νŠΈ κΈ°λŠ₯을 NuxtJS λ₯Ό μ‚¬μš©ν•˜λŠ” ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ”, Laravel Echo 의 NuxtJS 용 wrapper λͺ¨λ“ˆ @nuxtjs/laravel-echo 을 μ„€μΉ˜ ν›„, nuxt.config.js νŒŒμΌμ„ μ•„λž˜μ™€ 같이 μ„€μ •ν•œλ‹€.

  :
  buildModules: [
    '@nuxtjs/dotenv',
    '@nuxtjs/eslint-module',
    '@nuxtjs/vuetify',
    [
      '@nuxtjs/laravel-echo',
      {
        broadcaster: 'socket.io',
        host: 'http://websockets.test:6001',
        plugins: ['~/plugins/echo.js'],
        // transports: ['websocket', 'polling'],
        authModule: true,
        connectOnLogin: true,
        disconnectOnLogout: true
      }
    ],
  ],
  :

plugins/echo.js μ—λŠ” μ•„λž˜μ™€ 같이 public 채널, Laravel notification 용 프라이빗 채널, 그리고 presence 채널을 μœ„ν•œ 3개의 λ¦¬μŠ€λ„ˆλ₯Ό μΆ”κ°€ν–ˆλ‹€. 각각의 채널은 use-case λ³„λ‘œ μΆ”κ°€ν™•μž₯ ν•  수 μžˆμœΌλ―€λ‘œ, κ°€μž₯ μ‹¬ν”Œν•œ ν˜•νƒœμ˜ working example 둜 보면 λ˜κ² λ‹€.

export default function({ $auth, $echo, store, app: { $toast } }) {
  $echo.channel('public').listen('PublicMessageSent', (event) => {
    console.log('data from public', event)
  })

  if ($auth.loggedIn) {
    $echo
      .private(`App.Models.User.${$auth.user.id}`)
      .notification((notification) => {
        console.log(notification)
        $toast.success(notification.message)
        store.commit('notifications/INCREASE_UNREAD_COUNT')
      })

    $echo
      .join('presence')
      .here((users) => {
        store.commit('online/SET_USERS', users)
      })
      .joining((user) => {
        store.commit('online/JOIN_USER', user)
      })
      .leaving((user) => {
        store.commit('online/LEAVE_USER', user)
      })
  }
}

2. Laravel Echo Server λ₯Ό μœ„ν•œ laravel-echo-server.json 파일의 λ‚΄μš©μ€ μ•„λž˜μ™€ 같이 μ„€μ •ν•œλ‹€.

참고둜, Laravel 의 config/database.php Redis μ˜΅μ…˜μ˜ prefix κ°’ 즉, config('database.redis.options.prefix') 의 값이 app λ‚΄ μ‚¬μš©ν•œ 채널λͺ…κ³Ό μ‘°ν•©ν•˜μ—¬ μ‚¬μš©λ˜λ―€λ‘œ, 이λ₯Ό frontend μ—μ„œλ„ λ™μΌν•œ μ΄λ¦„μœΌλ‘œ ν•΄λ‹Ή 채널을 μ§€μ •ν•˜λ €λ©΄ databaseConfig.redis.keyPrefix μ˜΅μ…˜μ„ 같은 κ°’μœΌλ‘œ μ…‹νŒ…ν•΄μ£ΌλŠ” 것이 μ’‹λ‹€. 그러면, Redis 에 μ‹€μ œ μ„€μ •λœ 킀값에 상관없이 PHP μ½”λ“œμ—μ„œλŠ” new PrivateChannel('chat'); 이라 μ‚¬μš©ν•˜κ³ , JS μ½”λ“œμ—μ„œλŠ” Echo.private('chat').listen() 라고 μ‚¬μš©ν•˜λŠ” 것이 κ°€λŠ₯ν•˜λ‹€.

{
  "authHost": "http://amuse.test",
  "authEndpoint": "/broadcasting/auth",
  "clients": [
    {
      "appId": "1111aaaa9999cccc",
      "key": "8888dddd2222bbbb4444ffff6666eeee"
    }
  ],
  "database": "redis",
  "databaseConfig": {
    "redis": {
      "port": "6379",
      "host": "redis",
      "keyPrefix": "laravel_database_"
    },
    "sqlite": {
      "databasePath": "/database/laravel-echo-server.sqlite"
    }
  },
  "devMode": true,
  "host": null,
  "port": "6001",
  "protocol": "http",
  "socketio": {},
  "secureOptions": 66666666,
  "sslCertPath": "",
  "sslKeyPath": "",
  "sslCertChainPath": "",
  "sslPassphrase": "",
  "subscribers": {
    "http": true,
    "redis": true
  },
  "apiOriginAllow": {
    "allowCors": true,
    "allowOrigin": "http://localhost:3000",
    "allowMethods": "GET, POST",
    "allowHeaders": "Origin, Content-Type, X-Auth-Token, X-Requested-With, Accept, Authorization, X-CSRF-TOKEN, X-Socket-Id"
  }
}

3. Laravel 7 이 μ•„λ‹ˆλΌλ©΄, CORS 섀정을 μœ„ν•΄ "fruitcake/laravel-cors": "^1.0" λ””νŽœλ˜μ‹œλ₯Ό μˆ˜λ™μœΌλ‘œ μΆ”κ°€ν•˜κ³ , config/cors.phpλ₯Ό μ•„λž˜μ™€ 같이 μ„€μ •ν•œλ‹€.

<?php

return [
    'paths' => ['*'],
    'allowed_methods' => ['*'],
    'allowed_origins' => ['*'],
    'allowed_origins_patterns' => [],
    'allowed_headers' => ['*'],
    'exposed_headers' => false,
    'max_age' => false,
    'supports_credentials' => false,
];

μœ„μ˜ μ„€μ •μœΌλ‘œ public, private, presence 채널 접속이 κ°€λŠ₯ν•˜λ©°, Load balancer λ₯Ό Echo μ„œλ²„ μ•žμ— μ„€μ •ν•˜λŠ” ꡬ성이라면 HTTPS λ₯Ό 톡신을 μœ„ν•œ Echo μ„œλ²„μ˜ μΆ”κ°€ μ„€μ • λ˜ν•œ ν•„μš”μ—†μ–΄μ§„λ‹€. 이제 NuxtJS μ•±μ—μ„œ, λ™μ μœΌλ‘œ 프라이빗 chat.{room id#} 채널을 listen() ν•˜κ±°λ‚˜ leave() ν•˜λ©΄μ„œ μ›Ήμ†ŒμΌ“ν†΅μ‹ μ„ ν•  수 μžˆλ„λ‘ λ§Œλ“€λ©΄, 비ꡐ적 μˆ˜μ›”ν•˜κ²Œ μ•„λž˜ κ·Έλ¦Όκ³Ό 같은 μ±„νŒ…μ„œλΉ„μŠ€λ₯Ό κ΅¬ν˜„ν•  수 μžˆλ‹€.

그럼 λ‹Ήμž₯ Laravel Echo κΈ°λŠ₯을 μ΄μš©ν•œ μ±„νŒ… μ„œλΉ„μŠ€λ₯Ό ν”„λ‘œλ•μ…˜μ— μ μš©ν•΄λ³΄μž. 라고 ν•  쀄 μ•Œμ•˜λ‹€λ©΄… Not so fast.

Posted by admin in Backend, 0 comments

Realtime broadcasting with Laravel and NuxtJS (1/5)

Prologue

Laravel 은 μ›Ήμ†ŒμΌ“μ„ μ‚¬μš©ν•œ μ‹€μ‹œκ°„ λΈŒλ‘œλ“œμΊμŠ€νŠΈ κΈ°λŠ₯을 “out of the box” 둜 μ§€μ›ν•œλ‹€. 이λ₯Ό μœ„ν•΄ Laravel Echo (https://github.com/laravel/echo) 라 ν•˜λŠ” ν΄λΌμ΄μ–ΈνŠΈ λΌμ΄λΈŒλŸ¬λ¦¬κ°€ ν”„λ‘ νŠΈμ•€λ“œ νŒ¨ν‚€μ§€μ˜ μΌλΆ€λ‘œ μ œκ³΅λ˜λŠ”λ°, 이λ₯Ό Nuxt ν”„λ‘œμ νŠΈμ—μ„œ μ‚¬μš©ν•  수 μžˆλ„λ‘ λ§Œλ“  wrapper λͺ¨λ“ˆμΈ @nuxtjs/laravel-echo (https://github.com/nuxt-community/laravel-echo) λ₯Ό μ΄μš©ν•˜λ©΄ Nuxt ν”„λ‘œμ νŠΈμ—μ„œλ„ μ‰½κ²Œ λ¦¬μ–Όνƒ€μž„ κΈ°λŠ₯을 μ‚¬μš©ν•  수 μžˆλ‹€.

곡식 λ¬Έμ„œμ— μ–ΈκΈ‰λ˜μ–΄ μžˆλ“―, Laravel μ—μ„œ 기본으둜 μ œκ³΅ν•˜λŠ” λΈŒλ‘œλ“œμΊμŠ€νŠΈ λ“œλΌμ΄λ²„λŠ” Pusher Channel κ³Ό Redis κ°€ μžˆλŠ”λ°, Pusher Channel 의 경우 νŠΈλž˜ν”½μ΄ λ§Žμ€ ν”„λ‘œλ•μ…˜ ν™˜κ²½μ—μ„œ μ‚¬μš©ν•˜κΈ°λŠ” λΉ„μš©μ μΈ λ¬Έμ œκ°€ μžˆμ–΄ κ°€μ„±λΉ„λ₯Ό 경쟁λ ₯으둜 μ‚Όμ•„μ•Ό ν•˜λŠ” μŠ€νƒ€νŠΈμ—… 인 경우, 썩 쒋은 μ˜΅μ…˜μœΌλ‘œ 보이지 μ•Šμ•˜λ‹€. λ”μš°κΈ° μ±„νŒ…κΈ°λŠ₯을 μœ„ν•΄ Pusher Chatkit κΉŒμ§€ μ‚¬μš©ν•΄μ•Ό ν•œλ‹€λ©΄, 가격 뢀담이 λ”μš± μ»€μ§€κ²Œ 느껴질 것이닀.

Pusher (https://pusher.com) 의 λ‹€λ₯Έ λŒ€μ•ˆμœΌλ‘œλŠ” Pusher API λ₯Ό PHP Ratchet (http://socketo.me) 을 μ΄μš©ν•˜μ—¬ κ°œλ°œν•œ Laravel WebSockets (https://docs.beyondco.de/laravel-websockets) 을 μ‚¬μš©ν•˜λŠ” 방법이 μžˆλ‹€. 이 νŒ¨ν‚€μ§€λ₯Ό μ‚¬μš©ν•˜μ—¬ μ›Ήμ†ŒμΌ“ μ„œλ²„ λ°•μŠ€λ₯Ό λ§Œλ“  λ‹€μŒ, Laravel 의 Pusher λΈŒλ‘œλ“œμΊμŠ€νŠΈ λ“œλΌμ΄λ²„κ°€, Pusher μ„œλ²„κ°€ μ•„λ‹Œ, μ»€μŠ€ν…€ 짝퉁 pusher λ°•μŠ€λ₯Ό μ΄μš©ν•˜λ„λ‘ λ§Œλ“œλŠ” 방법이닀. (step by step μ„€λͺ…은 μ•„λž˜ 유튜브 κ°•μ˜ μ°Έκ³ .)

ν•˜μ§€λ§Œ, 이 λ˜ν•œ νŠΈλž˜ν”½μ΄ λ§Žμ•„μ Έμ„œ ν•΄λ‹Ή μ„œλΉ„μŠ€μ˜ horizontal μŠ€μΌ€μΌμ΄ ν•„μš”ν•  λ•ŒκΉŒμ§€ κ³ λ €ν•œλ‹€λ©΄, ν”„λ‘œλ•μ…˜μš©μœΌλ‘œ viable path κ°€ 될 지 κ±±μ •μŠ€λŸ° 뢀뢄이 μžˆλ‹€. horizontal μŠ€μΌ€μΌμ„ μœ„ν•΄, ELB μ•„λž˜μ— μ—¬λŸ¬ μ„œλΉ„μŠ€ μΈμŠ€ν„΄μŠ€λ₯Ό μ—°κ²°ν•˜κ²Œ λœλ‹€λ©΄, κ²°κ΅­ peer μΈμŠ€ν„΄μŠ€λ“€ 간에 데이터 동기화λ₯Ό μœ„ν•œ 둜직이 ν•„μš”ν• ν…λ°, μ΄λŸ¬ν•œ μ»€μŠ€ν„°λ§ˆμ΄μ§•μ„ μœ„ν•˜μ—¬, PHP Ratchet 을 μ‚¬μš©ν•˜λŠ” 방법이 Node.js Express λ₯Ό μ‚¬μš©ν•˜λŠ” 방법 보닀 더 λ‚˜μ€ path 인지 μ—¬λŸ¬ μ΄μœ μ—μ„œ 확신이 μ„œμ§€ μ•Šμ•˜λ‹€.

또 λ‹€λ₯Έ λŒ€μ•ˆμœΌλ‘œλŠ” firebase λ₯Ό μ‚¬μš©ν•˜λŠ” 방법인데, μ˜† μ‚¬λ¬΄μ‹€μ—μ„œλŠ” 잘 μ‚¬μš©ν•˜κ³  μžˆλ‹€μ§€λ§Œ, 이 μ—­μ‹œ νŠΈλž˜ν”½ 증가λ₯Ό κ³ λ―Όν•  단계 μ―€ 되면 viable ν•œ μ†”λ£¨μ…˜μ΄ λ˜μ§€ μ•ŠλŠ”λ‹€λŠ” 뢀정적인 견해가 νŒ½λ°°ν•˜μ—¬, 기술적인 뢀뢄을 깊게 μ•Œμ•„λ³΄μ§€ μ•Šκ³  μ‰½κ²Œ 포기해 버렸닀. λ¬Όλ‘ , μ–΄λ– ν•œ λ¬Έμ œλ“€λ„ μ˜λ¦¬ν•œ work around 방법듀이 μ‘΄μž¬ν•˜κ² μ§€λ§Œ, ν˜„μž¬ λ‚΄κ°€ κ°–κ³  μžˆλŠ” λ¦¬μ†ŒμŠ€λ“€μ„ κ³ λ €ν•΄λ΄€μ„λ•Œ Laravel Echo Server (https://github.com/tlaverdure/laravel-echo-server) λΌλŠ” μ˜€ν”ˆμ†ŒμŠ€ ν”„λ‘œμ νŠΈλ₯Ό κ·Όκ°„μœΌλ‘œ κ°œλ°œν•˜λŠ” 것이 κ°€μž₯ ν˜„λͺ…ν•œ 방법이 되리라 νŒλ‹¨ν–ˆλ‹€.

μ„œλ‘κ°€ κΈΈμ—ˆλŠ”λ°, κ²°κ΅­ μœ„μ—μ„œ μ–ΈκΈ‰ν•œ μ΄μœ λ“€λ‘œ μΈν•˜μ—¬, AWS ν™˜κ²½μ—μ„œ ALB 와 AutoScaling κΈ°λŠ₯을 μ΄μš©ν•˜μ—¬ horizontal μŠ€μΌ€μΌμ΄ κ°€λŠ₯ν•œ Redis 와 NoSQL (MongoDB λ‚˜ DynamoDB 쀑 ν•˜λ‚˜) 을 μ‚¬μš©ν•˜λŠ” Laravel Echo ν˜Έν™˜ node.js μ„œλ²„μ˜ κ°œλ°œν•˜λ €κ³  ν•œλ‹€. λ‹€λ§Œ, 이λ₯Ό μœ„ν•œ μ•„λž˜μ— μ—΄κ±°λœ pre-requisite 듀이 Laravel/Nuxt 개발자인 λ‚˜μ—κ²Œ 만만치 μ•Šμ•„ λ³΄μ΄μ§€λ§Œ 말이닀.

  • node.js Express ν•™μŠ΅
  • TypeScript ν•™μŠ΅
  • Laravel Echo Server ν”„λ‘œμ νŠΈ μ†ŒμŠ€ 뢄석
  • Chat 용 인증 λ©”μΉ΄λ‹ˆμ¦˜
  • Redis pub/sub 용 데이터 뢄석/섀계
  • RDB/MongoDB 용 μŠ€ν‚€λ§ˆ 섀계
  • horizontal μŠ€μΌ€μΌ κ°€λŠ₯ν•˜λ„λ‘ λΆ€λΆ„ λ³€κ²½
  • ν…ŒμŠ€νŠΈ/배포

닀행인 것은 λΉ„μŠ·ν•œ 주제의 λ‹€μ–‘ν•œ 레퍼런슀 μžλ£Œλ“€μ€ μ¦λΉ„ν•˜λ‹€λŠ” 것.

Learning new things

Node.js ν•™μŠ΅μ€ 일단 Udemy κ°•μ’Œ 쀑 Node.js, Express, MongoDB & more: The Complete Bootcamp λ₯Ό μ„ νƒν•΄μ„œ λ“€μ—ˆλŠ”λ°, λ‚΄μš©μ΄ 42μ‹œκ°„μ΄ λ„˜λŠ” μ •λ„μ˜ λΆ„λŸ‰μ΄λΌ, 짧은 μ‹œκ°„λ‚΄μ— λͺ¨λ‘ λ“£κΈ°μ—” μ’€ 인내심이 ν•„μš”ν•˜λ‹€. ν•˜λ£¨ 8μ‹œκ°„ μ”© λ“£λŠ”λ‹€ 쳐도 6일 λΆ„λŸ‰. λͺ¨λ…Έν† λ„ˆμŠ€ν•œ λͺ©μ†Œλ¦¬ 톀 λ•Œλ¬Έμ— κΉœλ°• μ‘Έμ•˜λ‹€λ©΄, 어흑… μ„Ήμ…˜ 9κΉŒμ§€ λ“£κ³ , μ•„λž˜ μ„Ήμ…˜λ“€μ€ 일단 νŒ¨μŠ€ν•œλ‹€. μ‹œκ°„μžˆμ„ λ•Œ λ‹€μ‹œ!

https://www.udemy.com/course/nodejs-express-mongodb-bootcamp

  • μ„Ήμ…˜ 10. 인증
  • μ„Ήμ…˜ 11. λͺ½κ΅¬μŠ€ λͺ¨λΈλ§
  • μ„Ήμ…˜ 12. pug ν…œν”Œλ¦Ώ 엔진
  • μ„Ήμ…˜ 13. κ³ κΈ‰ κΈ°λŠ₯
  • μ„Ήμ…˜ 14. λ””ν”Œλ‘œμ΄λ¨ΌνŠΈ

TypeScript λŠ” Frontend Masters 의 κ°•μ’ŒμΈ TypeScript 3 Fundamentals, v2 λ₯Ό μ„ νƒν•΄μ„œ λ“€μ—ˆλ‹€.

https://frontendmasters.com/courses/typescript-v2/

이 κ°•μ˜λŠ” 4μ‹œκ°„ λΆ„λŸ‰μœΌλ‘œ μ˜€μ „ μž‘μ—… ν›„ 남은 ν•˜λ£¨ 일정에 도전해 λ³Ό λ§Œν•œ λΆ„λŸ‰μ΄λ‹€. TS 의 주된 νŠΉμ§•λ“€μ„ μ„€λͺ…ν•˜λ©΄μ„œ λ„˜μ–΄κ°€λŠ” κ²ƒκΉŒμ§€λŠ” μ’‹μ•˜λŠ”λ°, κ°•μ˜ λ‚΄μš©μ„ 기초둜 μ‹€μ œ ν”„λ‘œμ νŠΈλ₯Ό μ‹œμž‘ν•˜κΈ° 뢀쑱함이 λŠκ»΄μ Έμ„œ λ‹€λ₯Έ κ°•μ˜λ₯Ό ν•˜λ‚˜ 더 λ“£κ²Œ λ˜μ—ˆλ‹€. μ•„λž˜μ˜ Pluralsight 기초 TypeScript κ°•μ’Œ. μ΄λŸ¬λ‹€κ°€ μ‘°λ‚Έ κ°•μ˜λ§Œ λ“£λ‹€κ°€ μ—λ„ˆμ§€λ₯Ό μ†Œμ§„ν•˜κ³  끝내버릴 λΆ„μœ„κΈ°…

https://www.pluralsight.com/courses/typescript-projects-configuring-compiling-debugging

Time to get my hands dirty

λŠ˜μ–΄μ§„ μŠ€μΌ€μ₯΄λ‘œ 인해 μ œλŒ€λ‘œ 된 path λ₯Ό κ°€κ³  μžˆλŠ”μ§€ 확신이 쀄어듀긴 ν–ˆμ§€λ§Œ, 일단 VueJS λ₯Ό μœ„ν•œ JS μ½”λ”© μŠ€νƒ€μΌμ— λ§žμΆ°μ§„ λ‚˜μ˜ Visual Studio Code 의 ESLint 와 Prettier μ˜΅μ…˜λΆ€ν„° TypeScript λ₯Ό μœ„ν•΄ μˆ˜μ •ν–ˆλ‹€. .ts 파일 μ €μž₯도 μ œλŒ€λ‘œ μ•ˆλ˜λ©΄ μ•ˆλ˜λ―€λ‘œ. μ–΄μ¨Œλ“ , Visual Studio Code 의 settings.json 은 μ•„λž˜μ™€ 같이 λ³€κ²½ν•˜μ—¬, TypeScript λ₯Ό λŒ€μ‘ν•˜λ„λ‘ ν–ˆλ‹€.

{
  "": {
    "editor.formatOnSave": false
  },
  "": {
    "editor.formatOnSave": false
  },
  "[javascriptreact]": {
    "editor.formatOnSave": false
  },
  "[typescript]": {
    "editor.formatOnSave": false
  },
  "[typescriptreact]": {
    "editor.formatOnSave": false
  },
  "": {
    "editor.formatOnSave": false
  },
  "diffEditor.renderSideBySide": false,
  "editor.codeActionsOnSave": {
    "source.fixAll.eslint": true
  },
  "editor.formatOnSave": true,
  "editor.formatOnPaste": true,
  "editor.largeFileOptimizations": false,
  "editor.minimap.enabled": false,
  "editor.tabSize": 2,
  "eslint.alwaysShowStatus": true,
  "eslint.validate": [
    "vue",
    "html",
    "javascript",
    "typescript",
    "typescriptreact"
  ],
  "explorer.confirmDelete": false,
  "explorer.openEditors.visible": 0,
  "files.associations": {
    "*.module": "php"
  },
  "files.maxMemoryForLargeFilesMB": 20480,
  "php.suggest.basic": false,
  "phpcs.enable": false,
  "prettier.vueIndentScriptAndStyle": true,
  "sync.gist": "e38a9bd8b19391bb6ce89814e8e621f1",
  "terminal.integrated.fontFamily": "D2Coding",
  "terminal.integrated.fontSize": 13,
  "terminal.integrated.shell.osx": "/bin/zsh",
  "vetur.completion.useScaffoldSnippets": false,
  "vetur.validation.template": false,
  "window.zoomLevel": 0,
  "workbench.colorTheme": "Monokai",
  "workbench.iconTheme": "vscode-icons",
  "editor.detectIndentation": false,
  "html.format.preserveNewLines": false,
  "eslint.probe": [
    "javascript",
    "javascriptreact",
    "typescript",
    "typescriptreact",
    "vue"
  ]
}

Laradock to the rescue

λ‹€μŒ νŽΈμ—μ„œλŠ” λ‘œμ»¬ν™˜κ²½μ—μ„œ Laradock 을 μ‚¬μš©ν•˜μ—¬, ν”„λ‘œλ•μ…˜κ³Ό λΉ„μŠ·ν•œ ν™˜κ²½μœΌλ‘œFrontend, Backend 및 Laravel Echo Server λ₯Ό μ„€μ •ν•΄ 보겠닀.

Posted by admin in Backend, 0 comments

It's been a while to have something dockerized and deploy it in a production ready environment. To shape up my DevOp skills, I will try to set up a new ECS environment and blog what I did along the way. The application I am working on is built on top of the latest Nuxt JS. So, If you happened to follow something similar path, hope you find this somewhat helpful to better understand an option that I came up with.

Things that I have on my mind

  1. Build a front-end universal app using the latest NuxtJS and Vuetify and some other goodies I can think of.
  2. Build a back-end API using the latest Laravel 6
  3. Deploy the front-end to AWS ECS/Fargate
  4. Deploy the back-end to AWS EB

What I'd like to address this time is #3. The rest of the list have been addressed already and I've got solid work experience on the topics. When you have your own Docker container ready to use, you can deploy it to AWS ECS within a short period of time. you can even deploy it to AWS Elastic Beanstalk. But, ECS/Fargate route is what I've chosen to go with this time. To have a basic setup, read the article below along. Its step by step explanation is easy to follow.

https://itnext.io/run-your-containers-on-aws-fargate-c2d4f6a47fda

As with many other things in IT, however, there're a lot more to come. This one is no exception. Take a look at the following article written specifically on how to deploy NuxtJS app to ECS. The writer mentioned about runtime environment variable injection for the sake of security.

https://melvinkoh.me/dockerizing-and-deploying-nuxtjs-ssr-apps-to-aws-ecs-ck1egcwoi00dmjfs1w2cr3shq

With my current project, I've set up the following 5 variables in AWS System Manager Parameter Store. As you can see I added a prefix (prod-) to all of 'em.

So that I can easily write a rule to have an access right in the following fashion.

https://docs.aws.amazon.com/systems-manager/latest/userguide/sysman-paramstore-access.html

In case you wonder, my IAM ecs-task-execution-role has an inline policy like:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ssm:DescribeParameters"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ssm:GetParameters"
            ],
            "Resource": [
                "arn:aws:ssm:ap-northeast-2:123456000000:parameter/prod-*"
            ]
        }
    ]
}

But then, I confronted unexpected runtime environment variable issue with NuxtJS. A funny thing is that I've got no issues whatsoever on my local PC. But, NuxtJS didn't read the run-time environment variables in the production environment. WTF?! After further investigation,

https://github.com/nuxt/nuxt.js/issues/5100

I found the issue thread in its GitHub repo. As a workaround I was able to get around the problem by using samtgarson/nuxt-env and adding additional logic to my code base. Now, I can inject run-time environment variables with the package. As described in several posts in the thread, either ctx.app.$env.XXX or this.$env.XXX works depending on the usage.

Dockerizing Nuxt SSR app and deploy it to AWS ECS/Fargate
Posted by admin in Backend, 0 comments

Deploy Laravel 6 app to AWS Elastic Beanstalk

Mac 에 PHP 7.3 μ„€μΉ˜

μ‚¬μš©ν•˜λŠ” Mac 의 PHP 버젼이 behind 7.3 μ˜€κΈ°μ—, λ‹€μŒ 링크λ₯Ό μ°Έκ³ ν•˜μ—¬, Homebrew 둜 7.3 을 μ„€μΉ˜ν–ˆλ‹€.

https://stitcher.io/blog/php-73-upgrade-mac

μ•„λž˜ PATH λ₯Ό .zshrc 에 μΆ”κ°€ν•˜κ³ , php -v ν–ˆμ„λ•Œ 7.3.12 κ°€ 좜λ ₯λ˜λŠ” 것을 ν™•μΈν–ˆλ‹€.

  echo 'export PATH="/usr/local/opt/[email protected]/bin:$PATH"' >> ~/.zshrc
  echo 'export PATH="/usr/local/opt/[email protected]/sbin:$PATH"' >> ~/.zshrc

Mac 에 Python 3.7 μ„€μΉ˜

λ‚΄μΉœκΉ€μ— Python 도 μ—…κ·Έλ ˆμ΄λ“œ. μ•„λž˜ PATH λ₯Ό .zshrc 에 μΆ”κ°€ν•˜κ³ , 버젼을 ν™•μΈν–ˆλ‹€.

export PATH="/usr/local/opt/python/libexec/bin:$PATH"

Mac 에 EB-CLI μ„€μΉ˜

EB CLI κ°€ μ—†λŠ” PC μ˜€κΈ°μ—, λ‹€μŒ 링크λ₯Ό μ°Έκ³ ν•˜μ—¬, Homebrew 둜 CLI λ₯Ό μ„€μΉ˜ν–ˆλ‹€.

https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/eb-cli3-install-osx.html

Mac 에 AWS-CLI μ„€μΉ˜

brew install awscli λͺ…λ ΉμœΌλ‘œ μ„€μΉ˜ν–ˆλ‹€. IAM μ‚¬μš©μž 생성 ν›„, configuration ν•΄μ•Όλ§Œ λœλ‹€.

RDB 생성

Poorman’s DB Client 인 Sequel Pro λ₯Ό μ‚¬μš©ν•˜λŠ” 탓에 Free Tier 에 λ§žλŠ” template 을 μ‚¬μš©ν•˜μ—¬, RDB λ₯Ό μƒμ„±μ‹œ MySQL 5.7 버젼을 μ„ νƒν–ˆλ‹€. Public accessibility 은 YES 둜 μ„€μ •ν•˜μ—¬, Mac μ—μ„œ 접속이 κ°€λŠ₯토둝 ν–ˆλ‹€. (μ§€κΈˆκΉŒμ§€μ˜ μ„€μ •μœΌλ‘œ μ™ΈλΆ€ 접속이 κ°€λŠ₯ν•œλ“― μ‹Άμ—ˆμœΌλ‚˜, μΈλ°”μš΄λ“œ 3306 포트λ₯Ό μ—° μ»€μŠ€ν…€ VPC security group 을 μΆ”κ°€ν•˜μ§€ μ•ŠμœΌλ©΄ 접속이 λΆˆκ°€λŠ₯ ν–ˆλ‹€. default security group 의 μΈλ°”μš΄λ“œ/μ•„μ›ƒλ°”μš΄λ“œ κ·œμΉ™μ„ 보면 wide-open 으둜 λ˜μ–΄μžˆκΈ° λ•Œλ¬Έμ—, 접속이 λΆˆκ°€λŠ₯ν•œ μ΄μœ κ°€ λͺ…μΎŒν•˜κ²Œ μ΄ν•΄λ˜μ§€ μ•Šμ§€λ§Œ, anyway μƒˆλ‘œμš΄ μΈν”„λΌλ§ˆλ‹€ ν•„μš”ν•œ security group 을 μΆ”κ°€ν•΄μ•Ό ν•œλ‹€κ³  κ²½ν—˜μƒ μ•Œκ³  μžˆλ‹€. κ°€λ Ή redis λŠ” redis_sg λ₯Ό μΆ”κ°€ν•˜κ³  rdb λŠ” rdb_sg λ₯Ό μΆ”κ°€ν•œλ‹€.)

Elastic Beanstalk μ•± 생성

Web Server ν™˜κ²½μœΌλ‘œ μ½˜μ†”μ˜ μš”κ΅¬μ‚¬ν•­μ„ μž…λ ₯ν•˜μ—¬, High availability μ„€μ • preset 을 μ„ νƒν•˜λ©΄ Application Load Balancer λ₯Ό ν¬ν•¨ν•˜λŠ” PHP v7.3 μ•± ν™˜κ²½μ„ 생성할 수 μžˆλŠ”λ°, 이 섀정은 production μ΄λΌλŠ” μ΄λ¦„μœΌλ‘œ μƒμ„±ν–ˆλ‹€. λ‹€λ₯Έ λͺ¨λ“  μ˜΅μ…˜μ€ default 둜 μ„€μ •ν–ˆλ‹€. λ‘œλ“œ λ°ΈλŸ°μ„œκ°€ λ§Œλ“€μ–΄μ§€λ©΄, Route53 κ³Ό Aliased A λ ˆμ½”λ“œλ₯Ό λ„£μ–΄μ„œ μ—°κ²°ν•˜λ©΄ λœλ‹€. ACM (Amazon Certificate Manager) 으둜 μ„€μ •ν•˜λ©΄ https 에 ν•„μš”ν•œ certificate 에 ν•„μš”ν•œ CNAME λ ˆμ½”λ“œ 생성과 μžλ™κ°±μ‹ λ“±μ΄ μ§€μ›λ˜λ―€λ‘œ 관리적인 μΈ‘λ©΄μ—μ„œ μƒλ‹Ήνžˆ νŽΈλ¦¬ν•˜λ‹€.

EB CLI μ„€μΉ˜ 및 IAM λ©”λ‰΄μ—μ„œ μ‚¬μš©μž 생성

eb init 을 μ΄μš©ν•˜μ—¬, 졜초 μ΄ˆκΈ°ν™”λ₯Ό ν•˜λ € ν–ˆμ„λ•Œ, credentials 을 μ„€μ •ν•˜μ§€ μ•Šμ•˜λ‹€λŠ” μ—λŸ¬λ©”μ‹œμ§€κ°€ λ‚˜μ™”λ‹€. IAM λ©”λ‰΄λ‘œ 이동할 차둀이닀. Add user λ©”λ‰΄μ—μ„œ κ΄€λ¦¬μž 계정을 μƒμ„±ν•œλ‹€. 이λ₯Ό ν†΅ν•˜μ—¬, Access key ID 와 Secret access key λ₯Ό λ°›λŠ”λ‹€. μ΄λŠ” eb CLI 의 aws-access-id 와 aws-secret-key 둜 λŒ€μ‘λœλ‹€. (이런 μ΄λ¦„μ˜ λΆˆμΌμΉ˜λŠ” attention to detail competency κ°€ λ–¨μ–΄μ Έ λ³΄μ΄μ§€λ§Œ, anyway.) 이제, μ½”λ“œ λ² μ΄μŠ€μ— .elasticbeanstalk 폴더가 μƒμ„±λœ 것이 보인닀.

EB κ°€ μƒμ„±ν•œ EC2 λŠ” S3 와 SQS 에 λŒ€ν•œ access κΆŒν•œμ΄ μžˆμ–΄μ•Ό ν•˜λŠ”λ°, μ΄λŠ” IAM λ©”λ‰΄μ˜ Roles ν•­λͺ©μ—μ„œ aws-elasticbeanstalk-ec2-role 에 μΆ”κ°€ν–ˆλ‹€.

AWS Console μ—μ„œ production ν™˜κ²½ λ³€κ²½

production > configuration 에 container option 을 보면, Document root μ˜΅μ…˜μ΄ μžˆλŠ”λ°, 이λ₯Ό /public 으둜 μˆ˜μ •ν–ˆλ‹€.

ElastiCache Redis μ„€μ •

PHP 의 Redis 라이브러리인 predis/predis κ°€ 버렀진 ν”„λ‘œμ νŠΈκ°€ λ˜μ—ˆκΈ°μ—, EB ν™˜κ²½μ— php-redis/php-redis λ₯Ό μ†ŒμŠ€ μ„€μΉ˜λ₯Ό μœ„ν•œ μ•„λž˜μ˜ 섀정을 μΆ”κ°€ν–ˆλ‹€. Redis 섀정은 κ°„λ‹¨ν•˜λ―€λ‘œ μƒλž΅ν•œλ‹€. λ‹€λ§Œ, redis-sg λ₯Ό λ§Œλ“€μ–΄μ„œ, μΈλ°”μš΄λ“œ port λ₯Ό μ—΄μ–΄λ‘λŠ” κ±Έ μžŠμ§€ 말자. μ•„, 그리고 Laravel config/app.php 파일의 facade alias 인 Redis λ₯Ό 클래슀 μΆ©λŒμ„ λ°©μ§€ν•˜κΈ° μœ„ν•΄ RedisManager 둜 λ³€κ²½ν•˜λΌλŠ” λ§€λ‰΄μ–Όμ˜ 쑰언도 λ”°λžλ‹€.

# these commands run before the application and web server are
# set up and the application version file is extracted.
commands:
    01_redis_install:
        # run this command from /tmp directory
        cwd: /tmp
        # don't run the command if phpredis is already installed (file /etc/php.d/redis.ini exists)
        test: '[ ! -f /etc/php.d/redis.ini ] &amp;&amp; echo "phpredis extension not installed"'
        # executed only if test command succeeds
        command: |
            wget https://github.com/phpredis/phpredis/zipball/master -O phpredis.zip \
            &amp;&amp; unzip -o phpredis.zip \
            &amp;&amp; cd phpredis-* \
            &amp;&amp; phpize \
            &amp;&amp; ./configure \
            &amp;&amp; make \
            &amp;&amp; make install \
            &amp;&amp; echo extension=redis.so > /etc/php.d/redis.ini

AWS s3

λͺ¨λ‘ default μ˜΅μ…˜μ„ 선택후, λ²„ν‚·μ˜ public access 섀정을 μ•„λž˜μ˜ 링크λ₯Ό μ°Έμ‘°ν•˜μ—¬ μ…‹νŒ…ν–ˆλ‹€.

https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/user-guide/block-public-access-bucket.html

버킷은 environment λ³€μˆ˜ μ €μž₯용 1개, 이미지 μ €μž₯용 1개 톡합 2개λ₯Ό μƒμ„±ν–ˆλ‹€.

Laravel ν”„λ‘œλ•μ…˜ ν™˜κ²½μš© .env 파일 s3 μ—…λ‘œλ“œ

μ•„λž˜ λͺ…λ ΉμœΌλ‘œ .env νŒŒμΌμ„ s3 에 μ €μž₯ν–ˆλ‹€.

aws s3 cp .env.production s3://amuse-environment/.env

eb ssh μ…‹μ—…

eb ssh --setup 을 μž…λ ₯ν•˜κ³  ν™˜κ²½λͺ…을 μ§€μ •ν•˜λ©΄, λ‹€μŒκ³Ό 같이 ssh ν‚€λ₯Ό 생성후 μ—…λ‘œλ“œ ν•  수 μžˆλ‹€.

eb ssh production λͺ…λ ΉμœΌλ‘œ ν•΄λ‹Ή ν™˜κ²½ EC2 μΈμŠ€ν„΄μŠ€ ssh 접속이 κ°€λŠ₯ν•΄μ‘Œλ‹€.

.ebextensions 폴더 μ…‹μ—…

ꡬ체적인 셋업은 application specific ν•œ λΆ€λΆ„μ΄λ―€λ‘œ, codebase 내에 μžˆλŠ” .config νŒŒμΌλ“€μ„ μ°Έκ³ ν•˜κΈ°λ‘œ ν•œλ‹€.

s3 bucket policy

μ›Ήμ•±μ—μ„œ λ³΄μ—¬μ§ˆ images 폴더에 버킷 정책을 μ•„λž˜μ™€ 같이 μ„€μ •ν–ˆλ‹€.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject"
            ],
            "Resource": [
                "arn:aws:s3:::amuse-images/*"
            ]
        }
    ]
}

To-dos left

μ§€κΈˆκΉŒμ§€μ˜ μž‘μ—…μœΌλ‘œ, EB λ₯Ό μ΄μš©ν•˜μ—¬ ν•˜λ‚˜μ˜ μ½”λ“œλ² μ΄μŠ€λ‘œ μž‘μ—…λ˜μ–΄ μžˆλŠ” API 및 back-office 앱을 λ°°ν¬ν–ˆκ³ , μ΅œμ†Œν•œμ˜ κΈ°λŠ₯이 정상 λ™μž‘ν•˜λŠ” 것을 ν™•μΈν–ˆλ‹€. ν•˜μ§€λ§Œ, μ•„λž˜μ™€ 같은 μž‘μ—…λ“€μ΄ μΆ”κ°€λ‘œ μ§„ν–‰λ˜μ–΄μ•Όλ§Œ ν•˜λŠ”λ°, 이λ₯Ό μœ„ν•œ ν•„μš”ν•œ μ„€μ • 등은 κΈ°νšŒκ°€ 되면 λ‹€μŒλ²ˆμ— 올렀보기둜 ν•œλ‹€.

CloudFront ꡬ성

Posted by admin in Backend, 0 comments

Validating Android In-App Purchases with Laravel

Laravel 앱을 λ°±μ•€λ“œλ‘œ μ‚¬μš©ν•˜λŠ” μ•ˆλ“œλ‘œμ΄λ“œ ν”„λ‘œμ νŠΈμ—μ„œ, μΈμ•±κ²°μ œ ν›„ κ²°μ œμ •λ³΄λ₯Ό λ°±μ•€λ“œ API 을 μ΄μš©ν•˜μ—¬ μ‹œμŠ€ν…œμ— λ°˜μ˜ν•˜κ³  μžˆμ—ˆλŠ”λ°, μ–΄λŠμƒˆ ν•΄λ‹Ή API λ₯Ό μž„μ˜λ‘œ ν˜ΈμΆœν•˜λŠ” ν•΄ν‚Ή μ‹œλ„κ°€ λ°œμƒν–ˆλ‹€. 인앱 결제의 μœ νš¨μ„± 검증 단계 없이 μ €μž₯ν•˜λŠ” 이런 λ³΄μ•ˆμ— μ·¨μ•½ν•œ API λ₯Ό μœ„ν•˜μ—¬, 영수증 토큰값을 ꡬ글 API λ₯Ό 톡해 κ²°μ œκ°€ μœ νš¨ν•œμ§€ κ²€μ‚¬ν•˜λŠ” 검증 둜직과, μ •κΈ°μ μœΌλ‘œ ν™˜λΆˆ/μ£Όλ¬Έμ·¨μ†Œ/μ§€λΆˆκ±°μ ˆμ„ 톡해 λ¬΄νš¨ν™”λœ κ³Όκ±° 인앱 주문에 λŒ€ν•œ 관리 λ‘œμ§μ„ μΆ”κ°€ν–ˆλŠ”λ°, 이번 ν¬μŠ€νŒ…μ—μ„œ κ·Έ λ‚΄μš©μ„ μ‚΄νŽ΄λ³Έλ‹€.

μ•„λž˜ 링크둜 이동 ν•˜λ©΄ ꡬ글 개발자 μ½˜μ†”μƒμ—μ„œ ν•΄μ•Όν•  λ‚΄μš©λ“€μ„ μ°Ύμ•„λ³Ό 수 μžˆλ‹€. (μ§€λ§Œ, λ‚΄μš©μ΄ 많고 μž₯ν™©ν•œ μ„€λͺ…이 μ œκ³΅λ˜μ–΄, μ›ν•˜λŠ” λ‚΄μš©μ„ ν•œλˆˆμ— ν™•μΈν•˜κΈ°λŠ” 쉽지 μ•Šλ‹€.)

https://developers.google.com/android-publisher/getting_started?hl=ko#setting_up_api_access_clients

Google Developer Console

개발자 μ½˜μ†”μ— μ„€μ • > API μ•‘μ„ΈμŠ€ λ©”λ‰΄μ—μ„œ, 이미 λ“±λ‘λœ OAuth ν΄λΌμ΄μ–ΈνŠΈμ™€ μ„œλΉ„μŠ€ 계정이 μ—†λ‹€λ©΄, μƒˆλ‘­κ²Œ λ§Œλ“ λ‹€. κΈ°λ“±λ‘λœ μ„œλΉ„μŠ€ 계정이 μžˆλ”λΌλ„, ν•΄λ‹Ή 계정에 μž¬λ¬΄μ •λ³΄λ₯Ό 볼수 μžˆλŠ” κΆŒν•œμ΄ μ—†λ‹€λ©΄ ν•΄λ‹Ή κΆŒν•œμ„ μΆ”κ°€ν•œλ‹€.

μ½˜μ†”μƒμ—μ„œ, μ„œλΉ„μŠ€ 계정을 μƒμ„±μ‹œ λ‹€μš΄λ°›μ€ JSON credentials νŒŒμΌμ€ μ„œλ²„μ˜ μ λ‹Ήν•œ μœ„μΉ˜μ— λ³΅μ‚¬ν•˜λ„λ‘ ν•œλ‹€. (본인의 경우 S3 에 μ €μž₯ν•œ λ‹€μŒ, deploy μ‹œμ— base_path() μœ„μΉ˜λ‘œ λ³΅μ‚¬ν•˜λŠ” 방법을 μ‚¬μš©ν–ˆλ‹€.)

이제 ꡬ글 Play API λ₯Ό ν†΅ν•˜μ—¬ 정상적인 결제 μš”μ²­μΈμ§€λ₯Ό κ²€μ¦ν•˜λŠ” API 와, μ·¨μ†Œλœ 결제 리슀트λ₯Ό λ°›μ•„μ˜€λŠ” API λ₯Ό μœ„ν•œ ν•Έλ“€λŸ¬λ₯Ό λ§Œλ“€μ–΄μ•Ό ν•˜λŠ”λ°, κ΅¬κΈ€μ—μ„œ μ œκ³΅ν•˜λŠ” μ•„λž˜ νŒ¨ν‚€μ§€λ₯Ό μΆ”κ°€ν•˜λ©΄, OAuth 토큰 관리 등에 κ΄€ν•œ λ‘œμ§μ„ 직접 λ§Œλ“€μ§€ μ•Šμ•„λ„ λ˜λ―€λ‘œ, κ°„λ‹¨νžˆ μž‘μ„±ν•  수 μžˆλ‹€.

composer require google/apiclient

μœ„ νŒ¨ν‚€μ§€λ₯Ό μ„€μΉ˜ ν›„, μž‘μ„±ν•œ ν•Έλ“€λŸ¬μ˜ λ‚΄μš©μ€ μ•„λž˜μ™€ κ°™λ‹€.

<?php

namespace App\Services;

use Exception;
use Google_Client;
use Google_Exception;
use Google_Service_AndroidPublisher;

class GooglePaymentHandler
{
    private $client;
    private $service;

    public function __construct(Google_Client $client)
    {
        $this->client = $client;
        $this->client->addScope(['https://www.googleapis.com/auth/androidpublisher']);
        $this->client->setAuthConfig(base_path() . '/google.credentials.json');
        $this->client->setIncludeGrantedScopes(true);
    }

    /**
     * 결제 검증 API
     * @param string $appName, (e.g. `κ°œλ‚˜μ†Œλ‚˜`)
     * @param string $productCode, μ•ˆλ“œλ‘œμ΄λ“œ μƒν’ˆμ½”λ“œ (e.g. `com.whatever.history.06`)
     * @param string $purchaseToken, μ•ˆλ“œλ‘œμ΄λ“œ 결제 영수증 μ½”λ“œ
     * @return object|null
     * @throws Google_Exception
     */
    public function verify(string $appName, string $productCode, string $purchaseToken): ?object
    {
        $this->client->setApplicationName = $appName;
        $this->service = new Google_Service_AndroidPublisher($this->client);

        return $this->service->purchases_products->get(
            $this->getPackageName($appName), // com.some.thing
            $productCode,
            $purchaseToken
        );
    }

    /**
     * μ·¨μ†Œν•œ 결제 리슀트 API
     * @param string $appName
     * @return object|null
     * @throws Exception
     */
    public function voidedPurchases(string $appName): ?object
    {
        $this->client->setApplicationName = $appName;
        $this->service = new Google_Service_AndroidPublisher($this->client);

        return $this->service->purchases_voidedpurchases->listPurchasesVoidedpurchases(
            $this->getPackageName($appName) // com.some.thing
        );
    }

    /**
     * convert app-name to package-name (com.some.thing)
     *
     * @param string $appName
     * @return string|null
     */
    private function getPackageName(string $appName): ?string
    {
        if ($appName === 'κ°œλ‚˜μ†Œλ‚˜') {
            return 'com.some.thing.xxx';
        }
        if ($appName === '돈크라이') {
            return 'com.some.thing.yyy';
        }
        if ($appName === '함ν₯차사') {
            return 'com.some.thing.zzz';
        }

        return null;
    }
}

각 API λ‘œλΆ€ν„° λ¦¬ν„΄λ˜λŠ” 객체에 λŒ€ν•œ μ„€λͺ…은 μ•„λž˜μ˜ λ§ν¬μ—μ„œ λ³Ό 수 μžˆλ‹€.

https://developers.google.com/android-publisher/api-ref/purchases/products/get
https://developers.google.com/android-publisher/api-ref/purchases/voidedpurchases/list

일반적이라면, verify() API 의 경우, 맀 결제 μš”μ²­μ‹œλ§ˆλ‹€ ν˜ΈμΆœν•˜μ—¬, κ·Έ validity λ₯Ό κ²€μ‚¬ν•˜λ„λ‘ λ§Œλ“€κ³ , voidedPurchases() API 의 경우, CRON scheduler 둜 λ“±λ‘ν•œ Console Command 둜 μƒˆλ²½μ‹œκ°„μ— 1번 ν˜ΈμΆœλ˜λ„λ‘ ν•˜μ—¬, λ¬΄νš¨ν™”λœ κ²°μ œμ— κ΄€λ ¨λœ μ‚¬μš©μž ꡬ맀 μƒνƒœλ₯Ό λ³΄μ •ν•˜λ„λ‘ ν•˜λ©΄ λ˜κ² λ‹€.

Happy Coding!

Posted by admin in Backend, 0 comments