Next.js 및 Firebase Hosting SWR을 사용합니다.그나저나 GraphiQL과 Cloud Run

개요


며칠 전 넥스트.저는 js를 Firebase Hosting + Cloud Function For Firebase로 디자인해 봤어요.
왜냐하면 당분간 방문하지 않을 때 방문하면 상당히 엄격하거든요.
Firebase Hosting+SWR로 서버측 처리가 없으면 개발이 안 되는지 해봤어요.
https://zenn.dev/ucwork/articles/67663455297629
GraphiQL(gqlgen)으로 뒷면의 API를 제작하는 김에 Terraform을 통해 클라우드 런에 디자인하기
swr
다 쓰면 길어질 수 있으니 나중에 반복할 수 있도록 요점을 미리 적어두세요

로컬 환경


지금까지 비즈니스에서 프런트엔드와 백엔드(API)는 모두
왜냐하면 저는 같은 창고에 저장된 상황만 겪었거든요.
이번 실험적 으로 창고 를 분리하여 각각 독립해 보았다

백엔드(API)


GraphiQL(gqlgen) 만들기


업무상 REST만 접했지만 개인적으로는 그래피QL의 지식 수준을 향상시키고 싶으니 그렇게 하자
이것저것 돌아다니는 gqlgen이 편할 것 같아서 이걸로 적당한 API를 만들었어요.
https://gqlgen.com/
상세히 기재되지는 않았지만 대체로 이렇게 만들었다
local
go mod init github.com/shintaro-uchiyama/xxx
go get github.com/99designs/gqlgen
go run github.com/99designs/gqlgen init
schema.graphqls의 패턴을 원하는 것으로 변경
gqlgen을 통해 코드 다시 생성
local
go run github.com/99designs/gqlgen generate
resolver.go 응답하는 struct 지정schema.resolvers.go에 가상 응답 미리 쓰기
※ 파이어스토어 근처 DB에서 획득

docker-compose를 통해 로컬 환경 만들기


GraphiQL을 움직이기 위한 구글 Docker 준비
build/api/Dockerfile
FROM golang:1.15.6 AS local

WORKDIR /go/src/app

RUN go get github.com/cespare/reflex
CMD reflex -r '(\.go$|go\.mod)' -s go run server.go
docker-compose로 이 녀석을 깨워라
docker-compose.yml
version: '3.8'
services:
  api:
    build:
      context: ./build/api
      target: local
    ports:
      - "8080:8080"
    volumes:
      - ./:/go/src/app
방문http://localohst:8080 이런 느낌으로 GraphiQL 콘솔이 나오면 OK!
조회를 실행하고 결과를 얻은 후 마치 일하는 것 같다🧔
graphql
완전한 수다(GraphiQL의 Group By)
"지난주 분류별 총 매출액을 차트에 표시하고 싶어요".
GraphiQL에서 SQL이 말하는 Group By(Aggregation)는 어떻게 표현됩니까?
이런 느낌의 Group By의 집계 검색어를 어떻게 쓸까 한두 시간 고민했어요.
SELECT category_id, date, SUM(quantity) FROM sales WHERE date > DATE_SUB(DATE_TRUNC(CURRENT_DATE('Asia/Tokyo'), week), INTERVAL 1 week) GROUP BY category_id, date 
issue에서 보듯이 프로그램 내의 함수로 복잡한 통계를 진행할 필요가 없다
Schema로 표현하기로 했습니다.(의도와 일치하는가
query findCategories {
  categories {
    id
    name
    products {
      id
    }
    lastWeekSales{
      soldAt
      quantity
    }
  }
}
https://github.com/graphql/graphql-js/issues/855#issuecomment-302413633

프런트엔드


docker-compose를 통해 로컬 환경 만들기


이동가능next 용기 만들기
이번 공연은 본공연 때 Firebase Hosting을 사용했는데, 원래는 node였다.js 컨테이너 아니에요.
정적 출력 파일만 돌려주는 용기를 준비해야 할 것 같아요.
먼저 개발하고 싶어요next dev. 그래서 이쪽으로 가세요.
docker/nextjs/Dockerfile
FROM node:15.4.0-alpine3.10

WORKDIR /usr/src/app
제작된 Docker file을 docker compose에서 호출
docker-compose.yml
version: '3.8'
services:
  nextjs:
    build: ./docker/nextjs
    container_name: nextjs
    volumes:
      - ./:/usr/src/app
    command: "npm run dev"
    ports:
      - "3000:3000"
    networks:
      - ucwork-api_default
networks:
  ucwork-api_default:
    external: true
하나의 점으로 앞의 docker-compose에서api의 docker-compose에 접근할 수 있습니다
하고 있는 일docker network ls에 나타나는 API의 네트워크를 지정합니다.
※ 단, 이번 정식 공연은 노드입니다.js 환경이 없기 때문에, 실제로는 아무런 관계가 없다
※ Proxy를 원한다면 여기서 하는 게 좋을 것 같아요.
$ docker network ls
NETWORK ID     NAME                      DRIVER    SCOPE
91bf2ecd2671   ucwork-api_default        bridge    local

SWR을 통한 API 요청


사실 이거 해보고 싶었는데 한참 돌아다녔어요...

환경에 맞게 API URL 변경하기


정말 이런 느낌 /api/proxy/ 서로 다른 환경의 URL에서 대리하고 싶지만
이번에는 노드다.js 환경이 없어서 포기했어요.
next.config.js
module.exports = {
  async rewrites() {
    return [
      {
        source: "/api/proxy/:path*",
        destination: `${process.env.API_URL}/:path*`,
      },
    ];
  },
};
client 측면에서도 읽을 수 있는 환경 변수로 설정
next.config.js
module.exports = {
  env: {
    API_URL: process.env.API_URL,
  }
};
API_URL은 다음과 같이 설정됩니다.
.env.development
API_URL=http://localhost:8080
npx dev는 읽기 용이.env.development

swr 실행


이런 느낌으로 Hooks 디렉터리에서 분리해서 swr로 실행합니다.
상품 일람표 가지고 올게요.
hooks/useProducts.ts
import useSWR from "swr";
import { request } from "graphql-request";
import { Product } from "./useProduct";

interface Products {
  products: Product[];
  isLoading: boolean;
  error: any;
}

export const useProducts: () => Products = () => {
  const fetcher = (query) => request(`${process.env.API_URL}/query`, query);
  const { data, error } = useSWR(
    `{
      products {
        janCode
        name
        price
        registeredAt
      }
    }`,
    fetcher
  );

  return {
    products: data ? data.products : [],
    isLoading: !error && !data,
    error: error,
  };
}
제품 종류는 다른 파일로 정의합니다
hooks/useProduct.ts
export interface Product {
  janCode: string;
  name: string;
  price: number;
  registeredAt: string;
}
component에서 hooks 호출
components/organisms/ProductTable.tsx
import { useProducts } from "../../hooks/useProducts";
const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    table: {
      minWidth: 650,
    },
  })
);

export const ProductTable: React.FC<{}> = () => {
  const classes = useStyles();
  const [t] = useTranslation();

  const { products, isLoading, error } = useProducts();
  if (isLoading) {
    return <CircularProgress />;
  } else if (error) {
    return <div>{t("products.table.errors.load")}</div>;
  } else {
    return (
      <TableContainer component={Paper}>
        ...
	  <TableBody>
            {products.map((row) => {
              const registeredAt = new Date(row.registeredAt);
              return (
                <TableRow key={row.name}>
                  <TableCell align="right">{row.name}</TableCell>
loading 및 오류 및 데이터를 반환할 때
너무 쉬워서 좋아요.캐시된 물건을 사용하기 때문에 매우 빠르다

CORS 대책


오리진http://localhost:8080CORS에 API 요청을 해서 그런가 봐요.
GraphiQL 서버에 CORS 라이센스 설정이 추가되면 위에서 붙여넣은 대로 API 요청을 적절하게 수행할 수 있습니다
※ 모두 허가하긴 했지만, 대상의 URL을 사용해 선별🙇‍♂️
server.go
package main

import (
	"log"
	"net/http"
	"os"

	"github.com/go-chi/chi"

	"github.com/rs/cors"

	"github.com/99designs/gqlgen/graphql/handler"
	"github.com/99designs/gqlgen/graphql/playground"
	"github.com/shintaro-uchiyama/ucwork-api/graph"
	"github.com/shintaro-uchiyama/ucwork-api/graph/generated"
)

const defaultPort = "8080"

func main() {
	port := os.Getenv("PORT")
	if port == "" {
		port = defaultPort
	}

	router := chi.NewRouter()
	cors := cors.New(cors.Options{
		// FIXME: strict to target urls
		AllowedOrigins:   []string{"*"},
		AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
		AllowedHeaders:   []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
		ExposedHeaders:   []string{"Link"},
		AllowCredentials: true,
		MaxAge:           300, // Maximum value not ignored by any of major browsers
	})
	router.Use(cors.Handler)

	srv := handler.NewDefaultServer(generated.NewExecutableSchema(generated.Config{Resolvers: &graph.Resolver{}}))

	router.Handle("/", playground.Handler("GraphQL playground", "/query"))
	router.Handle("/query", srv)

	log.Printf("connect to http://localhost:%s/ for GraphQL playground", port)
	log.Fatal(http.ListenAndServe(":"+port, router))
}

브라우저에서 작업 확인


앞으로 docker-compose가 세워져 http://localhost:3000페이지가 표시되어야 합니다!
docker-compose up -d --build
자신의 상황은 이렇다
dashboard

정식 절차


백엔드(API)


Cloud Run으로 설계

Docker file 준비


다단계 구축에서 출력을 구축하고 실행하는 파일
build/api/Dockerfile
FROM golang:1.15.6 AS local

WORKDIR /go/src/app

RUN go get github.com/cespare/reflex
CMD reflex -r '(\.go$|go\.mod)' -s go run server.go

FROM golang:1.15.6 AS builder
WORKDIR /go/src/github.com/shintaro-uchiyama/xxx
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app server.go

FROM alpine:3.12.3 AS production
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/shintaro-uchiyama/xxx/app .
CMD ["/root/app"]

Container Registry 디자인


자체 GCP 프로젝트를 위한 설계
docker build --target production -t ucwork-api -f build/api/Dockerfile .
docker tag ucwork-api gcr.io/[project_id]/ucwork-api:0.1.0
docker push gcr.io/[project_id]/ucwork-api:0.1.0

Cloud Run에 대한 디버그


Cloud Run의 module 제작
약간의 검증이 있어서 누구나 볼 수 있도록 인증을 취소합니다
modules/gcp/cloud_run/main.tf
resource "google_cloud_run_service" "default" {
  name     = "cloudrun-srv"
  location = "asia-northeast1"
  project = var.project

  template {
    spec {
      containers {
        image = "gcr.io/${var.project}/ucwork-api:0.1.0"
      }
    }
  }

  traffic {
    percent         = 100
    latest_revision = true
  }
}


data "google_iam_policy" "noauth" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

resource "google_cloud_run_service_iam_policy" "noauth" {
  location    = google_cloud_run_service.default.location
  project     = google_cloud_run_service.default.project
  service     = google_cloud_run_service.default.name

  policy_data = data.google_iam_policy.noauth.policy_data
}
모든 프로젝트를 펼칠 수 있도록
project_id를 변수로 설정
modules/gcp/cloud_run/variables.tf
variable "project" {
  description = "gcpの対象project id"
  type = string
}
공식 환경에서 이번 대상의 프로젝트를 사용합니다id 지정
environments/production/main.tf
terraform {
  required_version = "0.14.3"
}

module "cloud_run_api" {
  source = "../../modules/gcp/cloud_run"
  project = "[project_id]"
}
설계를 진행하다
cd environments/production
terraform init
terraform validate
terraform plan
terraform apply
표시된 Cloud Run에 액세스하는 URL
GraphiQL 콘솔이 뜨면 잘 될 것 같아요.

프런트엔드


package.json에 스크립트 추가


package.json
{
  ...
  "scripts": {
    "login": "firebase login --no-localhost",
    "build": "next build",
    "export": "next export",
    "deploy": "firebase deploy --only hosting",

Firebase에 로그인


다음 출력을 실행하는 URL을 통해 Google 계정에 로그인합니다.
터미널에 붙여넣기 로그인 성공!
npm run login

Firebase 구성


Firebase deploy 앞에build and export
정적 파일이 out 디렉터리로 출력되기 때문에 디버깅으로 지정합니다
왜냐하면 페이지를 넘기면 Firebase hosting이 404가 돼요.
rewrites의 설정을 기록합니다.스케줄러: 다른 방법이 있어?🤔
firebase.json
{
  "hosting": {
    "predeploy": "npm run build && npm run export",
    "public": "out",
    "rewrites": [
      {
        "source": "/dashboard",
        "destination": "/dashboard.html"
      },
      {
        "source": "/products",
        "destination": "/products.html"
      }
    ]
  }
}

실제 디버깅


디자인된 클라우드 런의 URL을env.제품으로 지정next start 또는 next export.env.production은 환경 변수로 간주되는 것 같습니다
.env.production
API_URL=https://cloudrun-srv-xxx.run.app
디버그 후 출력된 URL에 액세스하면 로컬에서 볼 수 있는 것처럼 페이지가 표시될 것입니다
npm run deploy
SWR 덕분인지 아무튼 빨라 보여요
lighthouse

총결산


Firebase Hosting Node만 표시됩니다.js를 사용하지 않으려고 시도하다
부인할 수 없는 것은 억지로 이루어진 느낌이다.왜냐하면 js 기능을 충분히 사용할 수 없는 느낌이 있어서.
저는 이제 버셀에 대해서 솔직히 depro 작전을 해보고 싶어요.😫

좋은 웹페이지 즐겨찾기