20200725のReactに関する記事は9件です。

DockerでTypeScript+Reactな環境構築(create-react-app)

目標

  • Dockerとdocker-composeを用いてcreate-react-appし、Reactアプリケーションの開発を始められる
  • かつ、TypeScriptでReactを動かす

前提

  • Dockerが使用可能である
  • docker-composeが使用可能である

手順

1. 作業ディレクトリの作成

作業するディレクトリを作成しましょう。
プロジェクト名はここではsample-appとします。

% mkdir sample-app
% cd sample-app

2. Dockernize

Dockerfileとdocker-compose.ymlを作成し、中身を追加します。

% touch Dockerfile
% touch docker-compose.yml

nodeのバージョンはこちらから新しめのものを選択すればいいと思います。

プロジェクトディレクトリ名が"sample-app"なので、コンテナ名・react-app名は"frontend"とします。

Dockerfile
FROM node:14.4.0-alpine3.10

ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR /usr/src/sample-app
docker-compose.yml
version: '3'

services: 
  frontend: 
    build: .
    environment: 
      - NODE_ENV=development
    volumes: 
      - ./:/usr/src/sample-app
    command: sh -c 'cd frontend && yarn start'
    ports: 
      - '3000:3000'
    tty: true
  • volumes: ホスト:コンテナ間でファイル・ディレクトリを同期(= マウント)する
    • これにより、ホスト側のエディター等で行った編集がコンテナ側にも反映されたり、docker-compose run コマンドでコンテナ側に生成されたファイル・ディレクトリがホスト側にも同様に生成されるようになる
  • sh -c 'cd frontend && yarn start': docker-compose upしたとき、react-appディレクトリに移動し、devサーバーを起動する
  • ports: 公開用のポートを、'ホスト:コンテナ'の書式で指定している。
    • ブラウザでローカルホストを開くときはホスト側に指定したポートを参照する(e.g. この場合、ブラウザからはlocalhost:3000で開発環境のアプリケーションにアクセスできる)
  • tty: 値をtrueにすることで、docker-compose upでコンテナを起動させた際にコンテナがすぐに終了してしまうのを防ぐ

3. ビルド

% docker-compose build
% docker-compose run --rm frontend sh -c 'npx create-react-app frontend --template typescript'
  • --rm : コマンド実行後にこのコンテナ(frontend)を削除する
  • --template typescript : .jsxファイルの代わりに.tsxファイルが生成され、ReactアプリケーションがTypeScriptで動くようになる

正常にビルドが完了した場合、以下のようなディレクトリ構成になります。
TSXファイル等はsrc以下に配置されています。

sample-app
├── Dockerfile
├── docker-compose.yml
└── frontend
    ├── node_modules
    ├── public
    ├── src   
    ├── .gitignore
    ├── README.md
    ├── package.json
    ├── tsconfig.json
    └── yarn.lock

4. アプリケーション起動の確認

下記コマンドでdevサーバーが起動します。

% docker-compose up

http://localhost:3000/
にアクセスし、App.tsxの内容が表示されていれば成功です。
該当ファイルをエディターで編集するとすぐに反映されることも確認できるはずです。

あとは自由に開発を進めていきましょう!

アプリケーションの停止は下記コマンドでできます。

% docker-compose down

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【React】基礎編 ⑶

新しく学んだことをアウトプットしています。

React.Fragment

JSXは複数の要素を持つことができないため、
<div>タグで複数の要素を格納するのが一般的でした。

ですが、HTMLで描画した時に、
本来は<div>タグが必要ない所にまで要素が付与されてしまうので、意外と困る時がありますよね。

そんな時に、使用するのが、React.Fragmentなのです。

React.Fragmentは<div>タグと同じ役割を担っており、
HTMLでは要素としてカウントされないので、使い勝手が良いです。

App.jsx
import React, { Component } from 'react';

class App extends Component {
  render() {
    return(
      // divの替り
      <React.Fragment>
        <User name={"Taro"} age={8}/>
      </React.Fragment>
    );
  }
}

export default App;

関数コンポーネント

コンポーネントには、
クラスコンポーネントと関数コンポーネントの2つがあります。

Reactでは、VirtualDomというDomが存在していて、その中でどのDomが変更になったのか管理していて、その変更点のみを実際のDomに反映していく機構があります。
それぞれのDomにkeyを与えてあげて、必要最低限なDomの範囲をReactで管理しています。

App.jsx
// クラスコンポーネント

import React, { Component } from 'react';

class App extends Component {

  const profiles = [
    { name: "Taro", age: 8 },
    { name: "Hanako", age: 5 },
  ]

  render() {
    return(
      <React.Fragment>
        {
          profiles.map((profile, index) => {
            return(
              // keyをindexで与えてあげる
              <User name={profile.name} age={profile.age} key={index} />
            );
          })
        }
      </React.Fragment>
    );
  }
}

//  関数コンポーネント

const User = (props) => { // 引数にpropsを持たせる
  return(
    <div>
      <p> I am {props.name} and {props.age} years old!</p>
    </div>
  )
}

export default App;

defaultProps

ReactのpropsにはdefaultPropsという機構があります。
propsを受け取るComponentにdefaultPropsを設定してあげる。

以下のように設定してあげると、
Noname さんの age がデフォルトで 1 と出力されます。

App.jsx
import React, { Component } from 'react';

class App extends Component {

  const profiles = [
    { name: "Taro", age: 8 },
    { name: "Hanako", age: 5 },
    { name: "Noname" }
  ]

  render() {
    return(
      <React.Fragment>
        {
          profiles.map((profile, index) => {
            return(
              <User name={profile.name} age={profile.age} key={index} />
            );
          })
        }
      </React.Fragment>
    );
  }
}

const User = (props) => { 
  return(
    <div>
      <p> I am {props.name} and {props.age} years old!</p>
    </div>
  )
}
// propsを受け取るComponentでdefaultPropsを設定
User.defaultProps = {
  age: 1
}

export default App;

propTypes

先ほどのdefaultPorpsと同様に、
ReactのpropsにはpropTypesという機構があります。
propsを受け取るComponentにpropsTypesを設定してあげる。

以下のように設定してあげると、
name や age に型を設定することができ、 validation をかけることができます。
型を設定しておくことで、 name に数値が入れられたり、age に文字列が入るのを防止してくれます。
また、 isRquired とすることで、 age が空の場合はエラーが発生するようになります。

App.jsx
imoprt React from 'react';
// prop-Typesをimport
imoprt PropTypes from 'prop-types';
// 省略


// 
User.propTypes = {
  name: PropTypes.string,
  age: PropTypes.number.isRequired,
}

以上

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React deploy to github static pages

githubに静的ページをdeployする方法

install the gh-pages

npm i gh-pages

package.jsonを開き、scriptsに以下のdeployの内容を追記
image.png

"scripts":{ ...,
    "deploy": " gh-pages -d build",
    "predeploy": "npm run build"
}
"homepage": "https://{githubのuser-name}.github.io/{project名}"

※pre-がついていたら、該当するコマンドが動く先にpre-コマンドが動く、後コマンドが動く
※project名は git remote -v で検索する。
※該当するhomepageがpublishedされる先

projectをbuildするため、以下のコマンドを叩く

npm run build

build後以下のようにbuildフォルダーが作成されることを確認する
image.png

buildされたものを配布する(deploy)

npm run deploy

実行後以下のようにpublishedが出力されたら、OK
image.png

明示されたhomepageに入ったら、配布されたことが確認できる

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

gitコマンド初心者覚書

git初心者、すぐに忘れるので覚書

  • githubで + New repositoryで、パブリックで nodeを選び、 できたリポジトリをローカルにクローンする。
$ yarn global add create-react-app

$ git checkout -b hello-world

$ create-react-app ファイル名


$ git clone https://github.com/----/----.git//githubに新しいリポジトリを作り、ローカルにgit clone する

$ cd 作りたいディレクトリ //移動

$ git status //
git add されているけどまだ git commit されていないファイルの一覧
編集・変更・削除されているが、まだ git add されていないファイルの一覧
Git管理されていない、かつ .gitignore で管理除外対象にもされていないものの一覧

$ git add .gitignore //管理外にする

$ git add //変更内容をインデックスに追加してコミット対象にする

$ git diff

$ git checkout -b hello-world //ブランチを作る

$ git commit //コミットする
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自粛期間中に制作した成果物まとめ

はじめに

新型コロナウイルスの影響で4月から休業を余儀なくされたので
個人制作物がそこそこ溜まってきました。

ここではその溜まった成果物をまとめておきます。

成果物まとめ

# 001 AdobeXD WEBデザインテンプレート(随時更新中)

AdobeXDで、Webとアプリデザインのテンプレートを制作しまとめてあるものになります。
モックアップを制作する際にご利用ください。
こちらはこれからも随時更新していく予定です。

[URL]
- xd_webdesign_sample
- Demo

# 002 React製 WhiteBoardアプリ

create-react-appベースで開発したWhiteBoard(付箋投稿)アプリです。
バックエンドはFirebase(リアルタイムデータベース)を使用。

[コア機能]
- 付箋リアルタイム投稿

[URL]
- react-board
- Demo

# 003 React製 SNS風アプリ

create-react-appベースで開発したSNS風アプリです。
バックエンドはFirebase(cloud Firestore)を使用。

[コア機能]
- Firebaseログイン(メール、Twitter、Google)
- お問い合わせフォーム(cloud Firestore使用)
- チャット機能(cloud Firestore使用)
- ニュースAPI情報取得・表示

[URL]
- engineer-chat
- Demo

# 004 React製 Firestorage画像アップロード

Firebaseの一機能Firestorage(画像を格納できる場所)に画像アップロード機能を実装しました。
※cloud Firestoreにpathをアップする機能は実装していません(こちらは今後の目標)

[コア機能]
- Firestorage画像アップロード

[URL]
- react-image-upload

# 005 Next.jsチュートリアルからアーキテクチャ設計

ReactフレームワークであるNEXT.jsのチュートリアルを一巡してから、自分なりにアーキテクチャを設計しました。
また独自のAPIも組み込んだコンポーネントも追加しています。

[URL]
- hello-next

その他

その他、VueやNuxt、ReactとLaravelのサンプルなど制作しています。

https://github.com/dai-570415

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Reactアクセシビリティ

Reactの記事を参考に初心者がなんとか覚書を書いています。
ってそういう意味だったのかー、と思いました。

  • Webアクセシビリティ a11yとも呼ばれる
    • Web Content Accessibility Guidelines (WCAG) Overview アクセシビリティを備えたウェブサイトを構築するためのガイドラインを提供
    • WAI-ARIA React においてほとんどの DOM プロパティと属性がキャメルケースである一方で、これらの属性は純粋な HTML と同じようにハイフンケース(ケバブケースやリスプケースなどとも言われる)である必要があります。
    • セマンティックな HTML
    • 正しい子要素の出力
Fragment.js
    class Columns extends React.Component {
      render() {
        return (
          <React.Fragment>
            <td>Hello</td>
            <td>World</td>
          </React.Fragment>
        );
      }
    }
- key 付きフラグメント
明示的に <React.Fragment> と宣言したフラグメントでは key を持つことができる
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

個人サイトをjQueryからReactに書き換え

uedayou.netで個人的に作成したWebアプリやら技術系同人誌を紹介するサイトを公開しています。
サイトというか、ほとんど1ページしかないのですが、だいぶ古いコードで、アプリが増えてきて更新し難くなってきたので、作り直すことにしました。

従来の構成

当時、あまりサイト更新に時間をかけたくなく、1ページにまとまって見れるリンク集に毛が生えた程度のものを作りたかったので、スタイルはBootstrap、PC、タブレット、スマートフォン、どの環境でもそれなりに見れるようにするためMasonryjQueryを使って、作品紹介の要素がきれいに並び変わるようにしていました。

使用モジュール
jQuery
Masonry
Bootstrap

uedayou.net

問題点

以下のような要素を作品分、1つのHTMLファイル内に記述する形なので、数が少ないときはよかったのですが、数が多くなるにつれて、コードの可読性が悪くなり、作品の順番を入れ替えるのも大変になってきました。

<div class="app-section col-lg-4 col-sm-6">
  <h4>鉄道駅LOD</h4>
  <div class="well">
    <div class="image">
      <img src="app_images/jrslod-image.jpg" class="img-responsive">
    </div>
    <div class="description">
      <p><a href="https://uedayou.net/jrslod/" target="_blank">鉄道駅LOD</a>」は日本の鉄道に関するオープンデータを収集して提供するサイトです。
      </p>
      ...
      <a class="btn btn-primary" href="https://uedayou.net/jrslod/" target="_blank">サイトを開く</a>
      <a class="btn btn-primary" href="https://qiita.com/uedayou/items/b5131b5ca930fe0bef69" target="_blank">解説記事</a>
    </div>
  </div>
</div>

新しい構成

昨今だと、GatsbyVuePressHexoなど静的サイトジェネレータを使うのが一般的かもしれませんが、上記のような構成だと静的サイトジェネレータ使うにはちょっと重いかなと思いました。

なので、Reactで既存のページを再現して、作品紹介の部分のみ、編集しやすくするようにしました。
スタイルはBootstrap から Material-UI、MasonryはReact用にreact-masonry-cssというのがあったのでこちらに変更しました。
また、作品紹介部分はMarkdownで書けると楽なので react-markdown を使うことにしました。

使用モジュール
React
react-masonry-css
Material-UI
react-markdown

インストール

React は CLI を使ってセットアップしました。

npx create-react-app uedayou.net
cd uedayou.net

以下のモジュールをインストールしました。

npm install react-masonry-css
npm install @material-ui/core
npm install react-markdown

作品紹介コンポーネント

作品紹介部分はほぼ共通化されているので、作品紹介用の共用コンポーネントを作成しました。

Product.jsx
import React from 'react'
import ReactMarkdown from 'react-markdown/with-html';
import { makeStyles } from '@material-ui/core/styles';

import Card from '@material-ui/core/Card';
import CardHeader from '@material-ui/core/CardHeader';
import CardMedia from '@material-ui/core/CardMedia';
import CardActions from '@material-ui/core/CardActions';
import CardContent from '@material-ui/core/CardContent';
import Button from '@material-ui/core/Button';
import Typography from '@material-ui/core/Typography';

const useStyles = makeStyles((theme) => ({
  media: {
    height: 0,
    paddingTop: '56.25%',
    backgroundPositionY: 0,
  },
}));

function Product(props) {
  const classes = useStyles();
  return (
<Card>
  <CardHeader
    title={props.title}
  />
  <CardMedia
    className={classes.media}
    image={props.image}
    title={props.title}
  />
  <CardContent>
    <Typography variant="body2" component="div">
      <ReactMarkdown
        source={props.description || ""}
        escapeHtml={false} />
    </Typography>
  </CardContent>
  <CardActions>
    {(props.links && props.links.length>0) 
      && props.links.map(v=>(
      <Button variant="outlined" color="primary" href={v.link} key={v.link} target="_blank">
        {v.label}
      </Button>
    ))}
  </CardActions>
</Card>
  )
}

export default Product;

これで、たとえば

JrsLod.jsx
import React from 'react'
import Product from './Product';

function JrsLod() {
  return (
<Product 
  title="鉄道駅LOD"
  image="app_images/jrslod-image.jpg"
  description={`
[鉄道駅LOD](https://uedayou.net/jrslod/)は日本の鉄道に関するオープンデータを収集して提供するサイトです。

鉄道会社、鉄道路線、鉄道駅のデータを閲覧、ダウンロードできます。鉄道会社のデータには、全路線のポリラインデータ、路線データには路線のポリラインデータ、駅データには、駅のポイントデータをダウンロードできます。
JSONデータでダウンロードでき、CORSにも対応しています。Webアプリから直接利用することができます。

[鉄道駅LOD](https://uedayou.net/jrslod/)のデータを使ったWebアプリ
[鉄道駅LOD GeoJSONダウンローダー](https://uedayou.net/jrslod-geojson-downloader/)
の
[ソースコード](https://github.com/uedayou/jrslod-geojson-downloader)
を公開しています。
「鉄道駅LOD」のデータを使ったアプリを作るときに参考にしてみてください。
`}
  links={[
    {
      label:"サイトを開く",
      link:"https://uedayou.net/jrslod/"
    },
    {
      label:"解説記事",
      link:"https://qiita.com/uedayou/items/b5131b5ca930fe0bef69"
    },
  ]}
/>
  )
}

export default JrsLod;

のように必要な部分だけをコンポーネントにかけるようになりました。
description部分は Markdown で書けるようにしています。
改善の余地はあると思いますが、個人でやってるサイトであれば必要十分かと思います。

ページへの登録

上記の作品紹介コンポーネントは、以下のように登録しています。

import React from 'react'
import Masonry from 'react-masonry-css'
import Container from '@material-ui/core/Container';
import { makeStyles } from '@material-ui/core/styles';

import Rosenoh from './products/Rosenoh';
import JrsLod from './products/JrsLod';
import LoajBook from './products/LoajBook';
...

const useStyles = makeStyles((theme) => ({
  myMasonryGrid: {
    display: "flex",
    marginLeft: "-30px",
    width: "auto",
  },
  myMasonryGridColumn: {
    paddingLeft: "30px",
    "& .MuiCard-root": { marginBottom: "30px" }
  }
}));

function Contents() {
  const classes = useStyles();
  const breakpointColumnsObj = {
    default: 3,
    1100: 2,
    700: 1
  };

  return (
<Container maxWidth="lg">
  <Masonry
    breakpointCols={breakpointColumnsObj}
    className={classes.myMasonryGrid}
    columnClassName={classes.myMasonryGridColumn}>
    <Rosenoh />
    <JrsLod />
    <LoajBook />
    ...
  </Masonry>
</Container>
  )
}

export default Contents;

<Masonry>...</Masonry> の間に作品紹介コンポーネントを記述します。
これで以下のように表示されます。

コンポーネントの順番を変えれば、ページでの表示順も変わりますし、削除も簡単です。
これで従来の構成よりもだいぶ編集が楽になりそうです。

新ページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TypeScriptでstyled-componentsを使う時propsでstyleの制御ができなくて困った時の解決法

概要

TypeScriptでstyled-componentsを使いたくて公式ドキュメントのコードを写経していたら、
"Adapting based on props"の節の以下のコードが通らなくて困った。

const Button = styled.button`
  /* Adapt the colors based on primary prop */
  background: ${props => props.primary ? "palevioletred" : "white"};
  color: ${props => props.primary ? "white" : "palevioletred"};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

原因

原因はpropsの部分に以下のエラーが出ていた。

> any
> プロパティ 'primary' は型 'ThemedStyledProps<Pick<DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>,
 "form" | ... 264 more ... | "value"> & { ...; }, any>' に存在しません。ts(2339)

解決法

interfaceを以下の様に定義してあげるとコンパイルが通った。

import styled, { StyledComponent } from 'styled-components';

interface ButtonProps {
  primary: boolean;
}

export const Button = styled.button<ButtonProps>`
  /* Adapt the colors based on primary prop */
  background: ${({primary}) => (primary ? 'palevioletred' : 'white')};
  color: ${({primary}) => (primary ? 'white' : 'palevioletred')};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid palevioletred;
  border-radius: 3px;
`;

ちなみにこのButtonコンポーネントを使う側のコードは以下の様になる。

import React from 'react';
import Button from '../../atoms/Button';

export const App: React.FC = () => {
  return (
    <>
      <Button primary={false}> Button </Button>
    </>
  );
};

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React(Next)/Go/GraphQL/Fargateで個人開発したサービス「読み方アンケート」をリリースするまで

個人開発である程度しっかりしたものを作ってリリースまでするってなかなか大変だと思います。

そこには技術的な壁がある人もいれば、なかなか本業が忙しくて時間がとれない人、途中まで開発して「こんなん誰が使うねん...」モードになってしまい頓挫する人。

そんないくつかの壁をなんとか乗り越え、
この度晴れて個人開発サービスである読み方アンケートをリリースできたのでまとめたいと思います。


〜こんな人に読んで欲しい〜
・ 個人開発のモチベーションをあげたい人
・ 個人でWebサービスをリリースするまでにするべきことを知りたい人
・ 以下の技術等に興味のある人
 ・ React(Next.js)
 ・ Go
 ・ GraphQL
 ・ AWS Fargate
 ・ AWS CodePipeline
 ・ GitHub Actions
 ・ TypeScript
 ・ Atomic Design
 ・ Recoil
 ・ Vercel

書くこと(index)

サービス概要

サービス名: 読み方アンケート
URL: https://yomikata-enquete.com/
yomikata-ogp.png

以下のように投票形式で読み方のデータを蓄積することで、世間一般的な読み方を知ることができる読み方情報蓄積サイトです。
スクリーンショット 2020-07-25 8.43.45.png

ITは特にそうですが、専門用語の読み方って人によって違ったり、「え、そんな読み方すんの!?」みたいなのもあってややこしすぎますよね...

Djangoを「でぃーじゃんご」って読んでて陰で笑われてたあの頃...

そんな恥ずかしい読み間違いとサヨナラしてもらうために作りました

積極的に使ってくれるユーザーには恩恵があるようにしたいという思いがあったので、1つのミソとしては上で貼った画像のように読み方を聞いた人のプロフィールは単語ページの上部に大きく表示されます。

読み方を調べようとページに訪れた人の目に止まるので、検索流入によるアカウントの宣伝権利を半永久的に獲得できるというわけです。

また、以下のように投票した数や、その人のおかげで集まった投票数もカウントしているのでこの数字が大きい人のアカウントも目立つようにするつもりです(リリース予定)
スクリーンショット 2020-07-22 18.01.22.png

「シェアして集めた投票数」はログインした状態で各単語ページのシェアボタンからシェアすると、そのリンク経由でページを訪れた人が投票するとカウントアップされていく仕組みです
スクリーンショット 2020-07-22 18.01.44.png

目指すところとしては
■ このサイトで調べればある程度世間一般的な読み方がすぐ分かる
■ 「◯◯ 読み方」で検索したときに検索結果上位にでるサイトにすること

という感じです。

是非使ってみてくださいm(..)m

サービス構成

技術的にどのような構成で作られているかを紹介します。

サービス構成概要

  • 開発環境
    • Docker
  • フロントエンド
    • React(Next)、TypeScript
  • バックエンド
    • Go
  • データベース
    • RDS(PostgreSQL)
  • CI
    • GitHub Actions
  • CD
    • CodePipeline
  • 画像保存
    • S3
  • バックエンドデプロイ先
    • ECS(Fargate)
  • フロントエンドデプロイ先
    • Vercel

フロントエンド

Reactで作成しています。

また、今回はSEOが重要だったためSSR(サーバーサイドレンダリング)が可能なフレームワークであるNext.jsを使用しています。

Nextは目を見張るスピードで進化していて、かなり使いやすいので使ったことない人は是非使ってみてください。

ルーティングの設定が不要ですし、最近(2020年5月)のリリース(ver9.4)で実装されたFast Refleshはかなり開発効率あがります。
コードを変更したときのホットリロードがかなり速く、しかもstateを保持したままリロードされます。

例えばこれまでフォームのテキストボックスとかに値を入力した状態でコード変更すると頑張って入力したのが全部消えていたのが、それらの情報は保持されたまま修正箇所のみ変更されます。はい、最高です。

他にも便利な機能目白押しですb

コンポーネントの設計はAtomic Designをベースに行っています。

適切な粒度でコンポーネントを分割することで、開発効率や保守性が高くなります。詳しくは後述します。


また、APIのやりとりはGraphQLで行っています。

TypeScriptとGraphQLの組み合わせはハッキリ言ってフロント開発の世界が変わります。RESTには戻れなくなります←

これもコードレベルの話は後述します。

バックエンド

バックエンドにはGoを使用しています。

普段は仕事でフロントエンドの開発をメインにしており、ずっとGoを使って自分で0からバックエンドの構築をしてみたいと思っていたので色々試せて面白かったです。

GraphQLを使用するので型付きの言語が相性が良く、マイクロサービスでサクッとAPIサーバーを立てるのにGoは適しているので今回はうまくハマりました。

DBとのやりとりの部分はGoのORM(オーアールマッパー)ライブラリであるgormを使用しました。

インフラ

マイクロサービス構築のノウハウが得たいというのもあり、フロントとバックは別々のサーバーにデプロイしようということは決めていました。

AWSをちゃんと自分で触ったことがなかったため、バックエンドはAWSを使って勉強を兼ねることにし、フロントはVercel(旧Zeit Now)にデプロイしています。

Vercelはフロントのデプロイ先としてめちゃくちゃオススメです。詳しくは後述します。

AWSでは主に以下のサービスを利用しています。
・Fargate(ECS)
・RDS(PostgreSQL)
・S3
・ECR
・ALB
・CodePipeline

AWS周りは事前の知識が皆無だったこともありかなり苦労しましたが、今回の開発を通してだいぶレベルアップできたかなと思います。個人開発の醍醐味ですね。

あとは、GitHubにpushした際にテストを実行したりするのにGitHub Actionsを使用しました。途中まではCircleCIを使ってましたが、やはりGitHub内で完結するのは魅力的です。

なぜリリースまで辿り着けたか

ちょっと技術的な話から離れて、個人開発をリリースまで持っていくためのTips的なものを紹介します。

モチベーションを明確にする

これは個人開発だけでなく趣味でチーム開発をする時とかでも大事なんですが、

この開発におけるwhyを明確にする

ということが大切です。

「開発におけるwhy = モチベーション」と思ってますが、大体は以下のどっちがかなと思います。

1.アイディアを形にしたい
 ・過程より結果重視
 ・使ってもらうことが最優先
 ・作る時ワクワク、これ使われないなーと思うと一気にやる気↓↓

2.開発を通して技術を学びたい
 ・結果より過程重視
 ・学習ありきの開発
 ・最悪使われなくても良いや

チーム開発の場合、上記のようなメンバーのwhyが揃ってないパターンは大体頓挫します。

どちらが良い悪いではなく、自分は何故個人開発をやっているのかを理解して常々選択の軸にすることでブレずにリリースまで持っていける可能性が上がります。

因みに自分は今回の場合、2が70%くらいの比重を占めています。

感覚としてこれ以上2の比重が高くなってしまうとモチベーションを保って開発を続け、リリース日を迎えることがむずかしいです。

タスクを見える化する

仕事だけでなく個人開発でも見える化はめっちゃ大事です。

見える化してないと夜パソコンの前に座ってもどうしてもやる気出てこなかったり、「どこ実装しようかな〜」って考えながら現状でできてるアプリを動かして目についたとこ実装していくみたいな無駄の多い開発になります。

そして「あ、こんな機能ありやな〜」て移動中とか思いついたアイディアが忘れ去られたりします...


具体的にはTrelloに代表されるようなタスク管理ツールを使います。

自分の場合はClickUpを使用しています。

ClickUpは無料で、プライベートな空間で使用できますし、タスクレーンのカスタムもできたり、Slack通知も簡単に設定できるので仕事でも重宝しています。

以下のようにレーンを分けたりラベルを付けて優先順位やタスクタイプを見える化します。(テンション上がる)(一部都合上隠しています)
スクリーンショット 2020-07-22 21.18.23.png

「その先」を妄想する

たぶん個人開発者あるあるだと思います←

サービスをリリースした未来を妄想します。

  • サービスが人気になって「◯◯作った人ですよね!」って認識される未来
  • そこで学んだ技術を生かして仕事で活躍できる未来
  • そのサービスがきっかけでバイネームで仕事を依頼される未来
  • 大当たりして個人開発で食べていける未来

冗談のように聞こえますが、個人開発をずっと続けれている人達はこの妄想力が段違いなんじゃないかと思ってます←

必須スキルかもしれません。

Webサービスをリリースするまでにすること

では個人でWebサービスを開発、リリースするまでどんなことをしなければいけないのか紹介します。

これから開発する方は参考にして頂ければと思います。

(抜け漏れある気がするのでコメントで補足してもらえると助かりますm(..)m)

※ * がついている項目は必須

システム構成を考える*

まずは当たり前ですが構成を考えます。

  • フロント、バックに何を使うか
  • DBは使うのか、使う場合はRDBにするのかNoSQLにするのか等
  • デプロイ先はどこにするのか

ワイヤーフレームを作成する

これは必須ではありませんが、サイトの設計書とも呼ばれるワイヤーフレームを考えた方が開発はスムーズに進みます。

手書きでざっくりでも良いですし、得意な人はXDやFigmaを使って作成しても良いでしょう。

因みに自分は今回は何にも作らなかったです←

なんか個人開発くらい場当たり的に画面作ってみたかったのです...

DB設計をする*

データベースを使用する場合はDB設計は必須です。

これは場当たり的にはやってはいけません

DB設計はどうしても数をこなさないと上手くできないと思いますが、考えることを諦めてはいけません。

実装したい機能からどんなテーブルが必要なのか、将来機能追加したときに破綻しないか等々考慮してテーブルや項目を洗い出していきましょう。

開発する*

ここまでで開発は進めれる状態になったのでつらつらと開発しましょう。

頑張りましょう。

頑張りましょう。。。

デプロイ先を考える*

世に公開するためにはどこかのサーバーに作成したアプリをデプロイしなければいけません。

使用している技術やCI/CDとの相性、ランニングコスト等比較して早い段階で決めておきましょう。

CI/CD環境の整備をする

継続して安定的なサービスを提供し続けるためには今やCI/CD環境は必須と言えます。

GitHub Actions、CircleCI、CodePipeline等々選択し、実装していきましょう。

404や500ページのことも考える

意外と盲点なのが存在しないページが開かれた場合の404ページと、サーバーエラーが発生した場合の500ページです。

一般的なサービスは大体このあたりも綺麗に作成されています。

因みにNextの場合、/pages/以下に404.tsx_error.tsxのようにファイルを作成しておくだけでカスタムできるので最高です。

参考までに読み方アンケートは以下のようにしています。
(500ページにTwitterのタイムラインを載せるTipsはnoteから学びました。サーバートラブルがあってもユーザーに状況を伝えることができます)
スクリーンショット 2020-07-22 22.32.13.png
スクリーンショット 2020-07-22 22.34.10.png

meta,titleタグを整備する*

Webサービスの場合、meta,titleタグの設定は必須です。

検索エンジンやブラウザに伝える情報を記述します。

これらはHTMLのheadタグの中に記述するもので、titleタグはページのタイトル情報でブラウザのタブに表示されるものがそうです。

metaタグは色々な種類があり、SNSにURLをシェアした際の画像や文章を設定するOGPの設定や、ページの説明文などを記述していきます。

faviconやtouch-iconを作成する*

faviconは以下のようなブラウザのタブ等に表示される画像です。間違ってもデフォルトのままリリースしないようにしましょう。
スクリーンショット 2020-07-24 14.01.59.png

apple-touch-iconはスマホやiPadでホーム画面に追加された時の画像を設定します。

faviconに比べ優先度は低いですが、設定しておくにこしたことはありません。(PWA化する場合は必ず設定しておきましょう)

以下のように180×180サイズを設定しておけば大丈夫(のはず)です。

<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />

各ブラウザ、端末での動作を確認する*

一通りシステムが形になってきたら各ブラウザや、スマホ等でレイアウト崩れが起きていないか、機能が正常に動くかを確認しましょう。

PCでスマホのレイアウトを確認する場合、各ブラウザのDeveloper Tool等を使うと思いますが、実機で見たらレイアウトが異なることがあるのでちゃんと実機で見てみることが大事です。

IEは、、、個人開発であれば無視しましょう←

外形監視を導入する

これは最初からやる必要はないと思いますが、個人開発でやってみておくと良い経験になるし仕事に生かせると思います。

Sentryに代表される外形監視ツールは、ユーザーが操作している際フロントエンドで何かエラーが起きたときにSlack等に通知をしてくれるものです。

フロントエンドのエラーはユーザーの使い勝手に直結するので、知らない間にエラーが起きていてユーザーが離れていくということを避けられます。

余裕があれば導入してみましょう。(個人開発レベルであれば無料で使えます)

ドメインを取得する*

https://yomikata-enquete.com/yomikata-enquete.comの部分です。

ドメインの取得は必須です。年間で数百円とかなので惜しまずに買いましょう。

色んなサービス経由で買えますが、私はお名前ドットコムで買っています。

どこでも対して差はないので大丈夫です。

Google Analyticsへの登録

Google Analyticsは必須ではないですが、絶対設定しておいたほうが良いです。

ユーザーが今何人サイトのどのページを見ているかリアルタイムで見れたり、日や月単位で何人に訪問されたのか、直帰率はどのくらいか等々分析することができます。

リリース情報を公開したあとなんかはずっとリアルタイムの数字見てワクワクできます。そりゃあもうビール3杯くらいそれでいけます。

Google Search Consoleへの登録

Google Analyticsがユーザーの分析なら、Search Consoleは検索エンジンの分析です。

どんな検索ワードでサイトが訪問されたのか、googleにindex(検索結果にでるように認識されていること)されているか、されていない場合はリクエストしたり等々

検索流入に対する対策をたてるのに使用できます。GAと並んでとても重要です。どちらも無料で使用できるので導入しておきましょう。



単に開発するだけでなく、上記のことを乗り越えて初めてリリースを迎えることができます。

頑張りましょう!b

フロントエンド

ではここからは実装レベルでの細かいTips等を紹介します。

ざっくりとの紹介になるので、気になる部分がある人は質問等で聞いてもらえたらと思いますm(..)m

フロントエンドでは以下のライブラリを使用しています(一部)

■ React、Next.js、TypeScript

このセットはかなりDX(Developer Experience)が向上します。

VueとReactを比較した記事も書いていてけっこう読んで頂いているので参考にしてみてください。

あらためてReactとVueを比較してみる〔2020年最新版〕

■ eslint、prettier

コード自動整形や静的解析のために使用。

ESLintとPrettierを連携してVSCodeで保存時に自動で整形されるための設定は以下で紹介しています。(GitHubのリポジトリもあります)

良い感じのReact開発をすぐに始めれるテンプレートプロジェクトを作ったので共有します

■ GraphQL

  • https://graphql.org/
    GraphQLはFacebookが開発したRESTに代わる新しいAPIの規格です。
    schema(スキーマ)と呼ばれるAPIの設定ファイルを基盤にAPIを作成していくことからスキーマ駆動開発と呼ばれる開発フローが可能になります。

  • https://www.apollographql.com/
    ApolloはGraphQLを使うための様々な便利な機能を提供しているライブラリで、クライアントもサーバーもサポートしています。キャッシュができたり、reactの場合、hooksライクにAPI実行できたりするので非常に便利です。以下のgraphql-codegenと組み合わせることでschema作成→API呼び出しが超簡単になります。

  • https://github.com/dotansimha/graphql-code-generator
    schema情報から、API実行するための口や型情報を自動生成してくれるためのライブラリです。
    以下に簡単なサンプルを紹介します。

1.schemaを定義
以下はIDから1つの単語を取得するクエリ定義の例です。

# queryはデータ取得。その他mutationやsubscription
schema {
  query: Query
}

# データ取得はQueryに定義する、引数や返却型を定義
type Query {
  word(wordID: Int!): Word!
}

# 型情報
type Word {
  wordId: Int!
  title: String!
}



2.クエリを定義する

schemaを基に取得するためのクエリを定義します。
RESTのようにデータを全て受け取るのではなく、ここでフロントが必要なデータのみ指定できるのもGraphQLの強みです。

word.graphql
query word($wordID: Int!) {
  word(wordID: $wordID) {
    wordId
    title
  }
}



3.自動生成する
以下のコマンドを実行するとschema情報を基にファイルが自動生成されます。(別途設定ファイルの記載は必要。package.jsonのscriptに定義すると楽)

graphql-codegen --config codegen.yml



4.生成されたファイル

以下のようなファイルが自動生成されます。

type Wordの型情報や、useWordQueryなどのAPI実行用の口ができていることが確認できます。

graphqlClientApi.tsx
import gql from 'graphql-tag';
import * as ApolloReactCommon from '@apollo/react-common';
import * as ApolloReactHooks from '@apollo/react-hooks';
export type Maybe<T> = T | null;
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
  ID: string;
  String: string;
  Boolean: boolean;
  Int: number;
  Float: number;
};

/** データ取得はQueryに定義する、引数や返却型を定義 */
export type Query = {
  __typename?: 'Query';
  word: Word;
};

/** データ取得はQueryに定義する、引数や返却型を定義 */
export type QueryWordArgs = {
  wordID: Scalars['Int'];
};

/** 型情報 */
export type Word = {
  __typename?: 'Word';
  wordId: Scalars['Int'];
  title: Scalars['String'];
};

export type WordQueryVariables = {
  wordID: Scalars['Int'];
};

export type WordQuery = { __typename?: 'Query' } & { word: { __typename?: 'Word' } & Pick<Word, 'wordId' | 'title'> };

export const WordDocument = gql`
  query word($wordID: Int!) {
    word(wordID: $wordID) {
      wordId
      title
    }
  }
`;

/**
 * __useWordQuery__
 *
 * To run a query within a React component, call `useWordQuery` and pass it any options that fit your needs.
 * When your component renders, `useWordQuery` returns an object from Apollo Client that contains loading, error, and data properties
 * you can use to render your UI.
 *
 * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
 *
 * @example
 * const { data, loading, error } = useWordQuery({
 *   variables: {
 *      wordID: // value for 'wordID'
 *   },
 * });
 */
export function useWordQuery(baseOptions?: ApolloReactHooks.QueryHookOptions<WordQuery, WordQueryVariables>) {
  return ApolloReactHooks.useQuery<WordQuery, WordQueryVariables>(WordDocument, baseOptions);
}
export function useWordLazyQuery(baseOptions?: ApolloReactHooks.LazyQueryHookOptions<WordQuery, WordQueryVariables>) {
  return ApolloReactHooks.useLazyQuery<WordQuery, WordQueryVariables>(WordDocument, baseOptions);
}
export type WordQueryHookResult = ReturnType<typeof useWordQuery>;
export type WordLazyQueryHookResult = ReturnType<typeof useWordLazyQuery>;
export type WordQueryResult = ApolloReactCommon.QueryResult<WordQuery, WordQueryVariables>;



5.APIを実行する

上記で作成したAPIから取得したデータを表示したい場合、以下のようにするだけです!
型情報を別途定義する必要もなければ、error情報やloadingの情報も含まれています。
これがめっっっっっちゃ楽です。
gif.gif

■ Atomic Design

Atomic DesignはアメリカのWebデザイナーBrad Frost氏が考案・提唱したデザインシステムです。

細かい説明は色々なサイトで紹介されているので省きますが、自分がフロント構築する場合はAtomic Designベースでやります。

「ベースで」というのが大事で、自分の中でコンポーネント分割はあくまで概念であって、厳密なルールを強制するものではないと思っています。

なので、ベースはありつつもプロジェクト毎にメンバーで話し合って適宜カスタムしていって良いと思います。



コンポーネント分割というと「再利用」というメリットがフォーカスされがちですが、TypeScriptと組み合わせることで実装に制約を注入できるというのも大きいです。

例えば以下のようにloadingというpropsを必須のpropsとして定義しておくことでボタンを配置するときに押下時のloadingを設定しないとエラーになるので実装者にルールを意識してもらえます。

MainButton.tsx
import React, { ReactNode } from 'react';
import { Button } from 'semantic-ui-react';

type Props = {
  children: ReactNode;
  loading: boolean;
  onClick: () => void;
};

export const MainButton = (props: Props) => {
  const { children, loading, onClick } = props;
  return (
    <Button loading={loading} disabled={loading} onClick={onClick}>
      {children}
    </Button>
  );
};

loadingを設定しないとエラーになる。
スクリーンショット 2020-07-24 16.12.33.png

■ Semantic UI React

CSSライブラリとしてSemantic UI Reactを使用しています。

BootstrapやMaterial UIだと人と被るしな〜と思っている人にSemantic UI Reactはオススメです。

サンプルの例が丁寧ですし、色の選択も豊富です。もちろんレスポンジブやGrid系の仕組みも提供されていますし、読み方アンケートのTOPページで実装しているようなアニメーションも簡単に実装できます。

■ styled-components

ReactではCSSのあてかたに様々なパターンがあり、宗教戦争がおきがちです←

自分はstyled-components(以下SC)を好んで使います。

CSS-in-JSの中でもSCを使用している理由はscssライクに使えるのでもし方針の変更があってもscss→SC、SC→scssといった変換が容易というのと、hoverなどの擬似要素やメディアクエリも同様に使えるところが大きいです。

以下は読み方アンケートで使用している、リンクテキスト用のコンポーネントの例です。

LinkText.tsx
import React, { ReactNode } from 'react';
import styled from 'styled-components';

import { COLOR } from 'util/const';

type Props = {
  children: ReactNode;
  onClick: () => void;
};

export const LinkText = (props: Props) => {
  const { onClick } = props;
  return <SLink onClick={onClick}>{props.children}</SLink>;
};

const SLink = styled.p`
  color: ${COLOR.LINK};
  cursor: pointer;
  margin: 0;
  &:hover {
    text-decoration: underline;
    color: ${COLOR.LINK_HOVER};
  }
`;

scssの書き方がそのまま使用できるのでhover時のスタイル変更もjs内で簡単に適用できます。

問題としてあげられるのが、SCを使うと外部ライブラリから読み込んで使用しているコンポーネントか、SCのコンポーネントかパっと見で分かりづらいので、自分はSプレフィックスを付けるルールをよく使います。

■ react-spinners

ローディングをちょっと工夫したい人は是非。
読み方アンケートだと以下のローディングがそうです。
gif.gif

■ Recoil

上記のローディングを表示する箇所に使ってみています。

以下のような状態用atomを定義して

loadingState.ts
import { atom } from 'recoil';

export const loadingState = atom({
  key: 'loadingState',
  default: false,
});

trueの場合、黒い背景(Dimmer)上にローディングアイコンが表示されるようにコンポーネントをセットしておきます。

あとは下記のように任意の場所でstateを設定することでグローバルな状態管理が可能になります。

import React from 'react';
import { Button } from 'semantic-ui-react';
import { useSetRecoilState } from 'recoil';

import { loadingState } from 'store/recoil/loadingState';

export const UserPublish = () => {
  const setRecoilLoading = useSetRecoilState(loadingState);

  const onClickButton = () => {
    setRecoilLoading(true);
  };

  return <Button onClick={onClickButton}>ボタン</Button>;
};

まだ複雑な状態管理は試してないですが、かなり可能性を感じます。

■ react-alert

hooksライクに使えるので重宝しています。

なんでこんなにstarが少ないんだろう..

バックエンド

■ Go

今回の規模のサービスでは特に困ることはありませんでした。ビルドが早いのは良いですね。

■ gorm

  • https://github.com/go-gorm/gorm
    ORMライブラリとしてgormを使用しています。goのORMでは1番シェアが大きいのではないかと思います。

DBとのやりとりがシンプルになりますし、素のSQLも簡単に実行できるので助かりました。

■ GraphQL

  • https://github.com/99designs/gqlgen
    GoでGraphQLを扱うためのライブラリとしてgqlgenを使用しています。 フロントエンドと同様、shemaファイルを基に型情報やresolverの情報を自動生成できるので重宝します。

また、GraphQL Playgroundが含まれているので、バックエンドだけ実装した後GUI上で簡単にAPIを実行した結果を確認できるのでわざわざAPI実行のために別のツールを導入する必要がなくなります。

■ jwt-go

一般的なJWT系のライブラリと同じ感じで使えます。

■ go-chi

■ CORS

  • https://github.com/rs/cors
    CORSの設定に使用。何もしないと別ドメインからのリクエストはエラーになるのでマイクロサービスの場合は設定必須。

インフラ-環境周り

読み方アンケートで設定しているインフラや環境周りを紹介します。

■ Docker

開発環境はDockerで構築しています。

nginxを含めてリバースプロキシすることでhttp://localhost:3000/ではなく、http://localhost/でアクセスできるようにしています。(色んなプロジェクトで3000使うので不便なんですよね...)

以下は読み方アンケートのdocker-composeファイルです。

docker-compose.yml
version: '3'
services:
  db:
    image: postgres:11-alpine
    volumes:
      - db:/var/lib/postgresql/data
      - ./docker/init:/docker-entrypoint-initdb.d
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    ports:
      - "5434:5432"
    container_name: yomikata-postgres

  nginx:
    image: nginx:stable
    container_name: nginx
    ports:
      - '80:80'
    volumes:
      - ./conf.d:/etc/nginx/conf.d
    links:
      - front

  front:
    build:
      context: .
      dockerfile: ./docker/front/Dockerfile
    container_name: yomikata-front
    hostname: front
    volumes:
      - .:/yomikata-enquete:cached
      - node_modules:/yomikata-enquete/node_modules:cached
    ports:
      - '3001:3000'

  api:
    build:
      context: .
      dockerfile: ./docker/api/Dockerfile
    environment:
      DB_INFO: host=db port=5432 user=postgres dbname=postgres password=postgres sslmode=disable
    ports:
      - "8081:8081"
    container_name: yomikata-api
    tty: true
    volumes:
      - .:/usr/src/app
    command: go run ./server/cmd/server.go
    depends_on:
      - db

volumes:
  db:
    driver: local
  node_modules:
    driver: local

■ AWS Fargate

バックエンドはFargateで構築しました。

初めてだったのでまぁまぁ大変でした...

手順もまとめているのですが、ここに書くと長くなるので別途記事にしようと思います。

■ AWS CodePipeline

CDのためにCodePipelineを使用しています。

GitHubのproductionブランチへのpushに反応して動き、ビルドやデプロイを自動でしてくれます。

また、Slackへの通知も簡単に設定できます。

読み方アンケートでは以下のように開始と終了の通知をSlackに送るようにしています。
スクリーンショット 2020-07-24 18.25.15.png

GitHub Actions

各ブランチをGitHubにプッシュした際にテストを実行するためにGitHub Actionsを使用しています。

/.github/workflows/にymlで設定ファイルを記述します。

読み方アンケートでフロントのテストを実行する場合は以下のようなファイルを定義しています。

front_test.yml
name: front_test

on: push

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup NodeJs
        uses: actions/setup-node@v1
        with:
          node-version: '10.x'
      - name: Cache dependencies
        uses: actions/cache@v1
        with:
          path: ~/node_modules
          key: ${{ runner.os }}-node-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.os }}-node-
      - name: Install Dependencies
        run: yarn install --frozen-lockfile
      - name: Run test
        run: yarn test

プルリクエストには以下のように表示されます。
スクリーンショット 2020-07-24 18.42.16.png

ちなみにGitHub Actionsの勉強で買った以下の本は読みやすくて良かったです。
GitHub Actions 実践入門
9784844378716.jpg

Vercel

フロントのデプロイ先にはVercel(旧Zeit Now)を使用しています。

GUI上でGitHubのリポジトリとの連携設定を簡単にできて、ブランチをpushする度に各ブランチに対応したPreview用のURLを生成してくれるのでチーム開発では力を発揮します。
(わざわざブランチをpullしてきてローカルで実行して確認しなくて良い)

  • 以下のように自動でVercelがコメントしてくれる スクリーンショット 2020-07-24 23.56.35.png

Next.jsを開発している会社と同じなのでNextのデプロイもめちゃくちゃ簡単ですし、ReactやVueのデプロイもほぼ0configで可能です。

SSL化もデフォルトでされており、別途証明書を発行する手間もありませんし、お名前ドットコムなど外部で取得したドメインを設定するのも簡単にできます。

気になるお値段ですが、


なんと個人利用の場合無制限で無料です


いや、太っ腹かよ( ゚д゚)

ぜひ使ってみてください。

さいごに

個人開発、大変ではありますがやはり実際のプロダクトを作ることで得られる経験というのは本を読んだりUdemyで勉強するのとは段違いだと思います。

仕事と違って誰にも合わせる必要なく自分の思うがままに進めれるのも良いですしね←

ちなみに次は React × AWS Amplify で何か作ってみるつもりです。


読み方アンケートや使用している技術に関して質問があればこの記事にコメントしていただくか、私のTwitterにDMでもかまわないのでお待ちしています。

個人開発がんばりたい人、個人開発の先駆者の方、フォローお待ちしております。お互い頑張りましょうb

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む