ume

React コンポーネントの粒度を自分の中でルール化する(個人開発)

前書き

reactに触れていて.

上記が自分の中で曖昧だったため、 ここで一度自分の中でルール化したいと思います.

前提

  • 個人開発におけるコンポーネントの粒度のルールを作成する.

  • パフォーマンス、拡張性、保守性、可読性、再利用性を考慮する

  • 正解がない以上あくまで個人的な意見です

結論

コンポーネントに分ける基準(下記に当てはまった時コンポーネントに分ける)
  1. コンポーネントが単一責任の原則を守れていない(1つのコンポーネントは複数の役割が混在している)

  2. 再レンダリングが起きた時、不必要な箇所も再レンダリングされている

  3. 簡単に思いつく範囲で「将来こんな要件入りそうだなぁ」と1~2個思いつく

  4. この処理「将来色んなとこで使えそう」と使えそうな状況が1〜2個思いつく

  5. もし2〜3日後同じコードを見た時、どういう挙動か直感的にすぐ理解できないだろうと思った時

どこからどこまでを1つのコンポーネントに切り出すかの基準.
  1. 単一責任の原則(1つのコンポーネントは1つの役割しか持たない)に沿って分割する.

  2. ツリー構造で表現できるように分割する

コンポーネントが単一責任の原則を守れていないとは?

⇨1つのコンポーネントに複数の処理が混ざっている

単一責任の原則が守っていないケース(ユーザーをapiで取得して表示する)
const UserInfo = ({ userId }) => {


 useEffect(() => {
   //①ユーザーを取得するapiの処理
  }, []);

return (

//②取得したユーザーを表示する処理

)
}

上記のようにUserInfoというコンポーネントの中に.

  • ユーザーを取得するapiの処理

  • 取得したユーザーを表示する処理

と2つの処理が入ってしまっている。

単一責任の原則を守っているケース
#ユーザーを取得する処理
const useUserData = (userId) => { 

 useEffect(() => {
   //①ユーザーを取得するapiの処理
  }, []);


}
#引数で与えられたユーザーを表示するコンポーネント
const UserInfo = ({ user }) => {
 
  return (
    <div>
      <h2>{user.name}</h2>
      <p>Email: {user.email}</p>
      <p>Bio: {user.bio}</p>
    </div>
  );
};

上記のようにユーザーを取得する処理を切り出してあげることで.
UserInfoコンポーネントの中の処理は「引数で取得したユーザーを表示する処理」.
だけに役割が1つになり単一責任の原則が守られていることになります.

単一責任の原則を守るメリット

下記が向上します.

  • 再利用性.

  • 拡張性.

  • 保守性.

  • パフォーマンス性.

  • テスト性.

  • 可読性.

詳しく単一責任の原則を知りたい方は下記を参照ください.

loveenglish.hatenablog.com

再レンダリングが起きた時、不必要な箇所も再レンダリングされているとは?

不必要な箇所の再レンダリングが起きているパターン(パフォーマンスが悪い)

Bの処理だけ再レンダリングしたいケースを考えます 現在Aコンポーネントの中に2つの処理(B,C)がある状態です.

const A = () => {
  return (
    <div>
      //Bの処理
      //Cの処理
    </div>
  );
};

この状態でAコンポーネントの中のBの処理を実行中に再レンダリングが発生したとします.
この時Aコンポーネントの中の処理が全て再レンダリングされます(BとCの処理が再び実行される).
これだとBの処理だけ再レンダリングして欲しいのに不要なCの処理(不必要な箇所)も.
レンダリングされてパフォーマンス性が悪いです

不必要な箇所の再レンダリングをさせないパターン(パフォーマンスが良い)
const A = () => {
  return (
    <div>
      <B />
      <C />
    </div>
  );
};

このように処理B,Cをコンポーネントにそれぞれ分割することで.
Bのコンポーネントの中で再レンダリングが起きてもCのコンポーネントは再レンダリングが発生しない(別途react.memoといった処理を記載する必要)ようにできパフォーマンス性を高めることができる

reactはコンポーネントどのように管理しているのか?

reactのコンポーネントは下記のようなツリー構造(トーナメントのような図)で認識される

例えば 以下のようにA,B,Cという3つのコンポーネントがあるとします.
Aコンポーネントの中にB,Cがそれぞれが子コンポーネントとして存在しているとします. この時3つのコンポーネントが上記の画像の真ん中のようにツリー構造で表現されます

const A = () => {
  return (
    <div>
      <B />
      <C />
    </div>
  );
};

この時Bコンポーネント内で再レンダリングが走る処理が加わるとどうなるでしょうか?
⇨Bだけ再レンダリングされます。コンポーネント内で再レンダリングが発生すると.
そのコンポーネントと子孫に再レンダリングが発生します. つまりAコンポーネントで再レンダリングが発生するとBとCが再レンダリングが発生します。

1. 簡単に思いつく範囲で「将来こんな要件入りそうだなぁ」と1~2個思いつく

例えば:ユーザーが商品のリストから欲しい商品を選択し.
カゴの中に選択した商品のリストと合計額を表示するアプリケーションがあるとします.

export const OkaimonoComponent: FC = () => {


  const productList = [
    { id: 1, category: "くだもの", price: 180, name: "りんご" },
    { id: 2, category: "くだもの", price: 460, name: "なし" },
    { id: 3, category: "くだもの", price: 2300, name: "すいか" },
    { id: 4, category: "やさい", price: 135, name: "きゅうり" },
    { id: 5, category: "やさい", price: 195, name: "きゃべつ" },
    { id: 6, category: "やさい", price: 98, name: "こまつな" },
  ] // get by API


//productListから選択した商品をカゴの中に入れて選択した商品と合計金額を表示する処理.  


}

上記の処理を1つのコンポーネントで作成した後に、将来、「同じ商品を複数個表示する機能」、「割引機能」 が実装されそうだなぁと予測が立ったらコンポーネントして細かく分けることを検討する

この処理「将来色んなとこで使えそう」と使えそうな状況が1〜2個思いつく

例えばこのようなボタンを実装していたとします その際将来、登録ボタン以外に、保存や削除といったボタンを使えそうと思いついたり.
将来実装されそうな機能に使えそうと予測がたてばコンポーネント化して再利用可能な状態にしておく

もし2〜3日後同じコードを見た時、どういう挙動か直感的にすぐ理解できないだろうと思ったとは?

例えば

import React, { useState, useEffect } from 'react';

const User = () => {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        setLoading(true);
        // ユーザーを取得する処理(仮のAPI)
        const response = await fetch('https://api.example.com/user/1');
        if (!response.ok) {
          throw new Error('Failed to fetch user data');
        }
        const data = await response.json();
        setUser(data);
      } catch (err) {
        setError(err.message);
      } finally {
        setLoading(false);
      }
    };

    fetchUser();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error}</div>;
  if (!user) return <div>No user data</div>;

  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {user.name}</p>
      <p>Email: {user.email}</p>
      <p>Phone: {user.phone}</p>
    </div>
  );
};

export default User;

上記のようにユーザーデータを取得する処理とユーザーデータを表示する処理があったとします。 このコード量と内容だとコードの理解が容易ですがもっと記述量が増えると「この処理なんの処理だったっけ?」と.
後からまたコードを1から読み直さないといけなそうです。
そこでコンポーネントに分割して、なんの処理かを直感的にわかるようにしたほうが開発効率が良さそうです。

どこからどこまでをコンポーネントで管理するか

下記のように画面を1つの役割ごとに分解していきコンポーネントごとの粒度を測るほうが良いと思いました。

まとめ

  • 正解がないような時は自分でルールを決めてそれに沿って進める.

  • reactはコンポーネントで処理を分けることでパフォーマンスを高めているので、コンポーネントに分けて開発しないとreactの良さを享受できない

参考情報

buildersbox.corp-sansan.com

非同期処理 実行順序

前書き

  • reactのアウトプットしている際に非同期処理の実行順序を理解していないためにハマったので, ここで理解を深めるために記事に残します。

対象者.

下記を見てどの順番でコンソールに文字が出力されるのかわからないかた

console.log(" グローバルコンテキスト");

setTimeout(() => {
  console.log(" マクロタスク (setTimeout)");
}, 0);

Promise.resolve().then(() => {
  console.log(" マイクロタスク (Promise)");
});

console.log(" グローバルコンテキスト終了");

答え

// " グローバルコンテキスト"
// " グローバルコンテキスト終了"
// " マイクロタスク (Promise)"
// " マクロタスク (setTimeout)"
なぜこのような答えになるのか?

⇨JSには実行順序がある。 実行順序.

グローバルコンテキストの処理⇨マイクロタスクの処理(promiseなど)⇨マクロタスク(setTimeoutなど)

グローバルコンテキストの処理が全て実行された後にマイクロタスクの処理に進み、マイクロタスクの処理が全て実行されると、マクロタスクの処理に進む.
⚠️マクロタスクの処理は1つずつ実行されるため、マクロタスクを実行しようとした時にマイクロタスクが突然増えるとマイクロタスクの処理が優先的に実行される。

グローバルコンテキストの処理とは?

⇨同期処理.
要は「その処理が終わるまで次の処理に進まない処理のこと」.
例.

console.log(1)
console.log(2)
console.log(3)
#実行結果1,2,3
#コンソールは同期処理なので上から1,2,3という順番に実行される
マイクロタスクとは?

⇨promiseやqueueMicrotaskなどの処理 例.

new Promise((resolve) => {
console.log("処理1")  #ここは同期処理として扱われる
resolve() #これはthenの処理に移行するための関数ですがthenはマイクロタスクの非同期処理として扱われるのでこの時点では他のグローバルコンテキストがある場合まだ実行されません。
}).then(()=>{
  console.log("処理2")})
console.log("処理3")
#出力結果 処理1、処理3、処理2 
マクロタスクとは?

⇨setTimeoutやsetIntervalなどの非同期関数 例.

function delayedGreeting(name) {
  setTimeout(() => {
    console.log(`こんにちは、${name}さん!`);
  }, 2000);
  
  console.log("setTimeoutがセットされました");
}

// 関数の使用例
console.log("プログラム開始");
delayedGreeting("太郎");
console.log("プログラム終了");
#出力結果 
1,"プログラム開始"
2,"setTimeoutがセットされました"
3,"プログラム終了"
4,"こんにちは、太郎さん!"(約2秒後)

応用

下記の実行の順番は?

async function func1 (){
  console.log("a")
  const hoge = await new Promise((resolve) => {
      resolve("")
  }).then(() =>
      console.log("b")
  )
}

function func2 (){
  new Promise((resolve) =>{
      console.log("c")
      setTimeout(() => {
    console.log("d")
    resolve(1)
},0)
  }).then(async() => {
      console.log("e")
      const fuga = await func1()
      }
  )
  console.log("f")
}

console.log("g")
func2()
console.log("h")

// 実行順序
// g -> c -> f -> h -> d -> e -> a -> b
解説

①まずはgが出力される。(それまでの関数は定義されているだけで実行されていない。).
②その後func2が実行される.
③cが出力される。promiseの中のthenが書かれている位置までは同期処理なので.
④fが出力される 理由setTimeoutはマクロタスクなのでこの時まだ実行されない。thenメソッドはマイクロタスクなのとsetTimeoutのresolve(1)が呼ばれたタイミングで実行されるのでまだこの時点では実行されない.
⑤hが出力、理由func2のsetTimeoutとthenの中の処理が飛ばされ。次の同期処理に処理が進む.
⑥dが出力 理由ここまででグローバルコンテキストに登録された処理が全て実行されたので次にマイクロタスクに処理が進むがsetTimeoutの中にマイクロタスク(then)があるのでマイクロタスクがないと解釈され、マクロタスク(setTimeout)の処理に進むのでdが出力.
⑦eが出力される。 理由resolve(1)が呼ばれ、処理がthenの処理に移行するので.
⑧aが出力 理由 eが出力された後に次の await func1()が実行されるので.
⑨最後にbが出力される

まとめ

  • jsには実行順序がある

  • // jsではグローバルコンテキスト→マイクロタスク(promiseのthenの処理など)→マクロタスク(setTimeoutなど)の順番で処理される。なのでグローバルコンテキストで登録された処理が全て処理された後にマイクロタスクが全て実行される。その後マクロタスクが1つずつ実行される

Docker✖️Rails8✖️Next.js15(SPA環境)の環境構築

前書き

  • 私の設定が間違っている場合ご指摘ください。

pc環境

全体像

  1. railsとnext.jsのプロジェクトをサブモジュール化

  2. railsの設定(データベース接続)

  3. docker-compose.ymlファイル作成

  4. フロントエンドとバックエンドをAPI連携

  5. 連携ができたか確認する

railsとnext.jsのプロジェクトをサブモジュール化

loveenglish.hatenablog.com

railsの設定(データベース接続)

全体像
  1. 環境変数を定義するファイル「.env」ファイル作成

  2. railsのconfig/database.ymlを編集

環境変数を定義するファイル「.env」ファイル作成

現状のディレクトリ構造

SpeCon/
│
├── back/
│   └── spe-con/ #⇦ここで.envファイルを作成する
│       └── (Railsプロジェクトのファイル群)
│
└── front/
    └── spe-con/
        └── (Next.jsプロジェクトのファイル群)

バックエンドのプロジェクトのルートで.envファイル作成

touch .env

.env(このように環境変数を定義) アカウント名、パスワード、ホスト名を定義してください

USER=任意のユーザー名(例:abcd)
PASSWORD=任意のパスワード(abcd123)
HOST=ホスト名(アプリ名)

注意:もし.envファイルを.gitignoreファイルに追加していない場合は.
.gitignoreファイルに下記を追加してください

/.env*

これでうっかり環境変数githubに載せるのを防ぎます。

またローカルの環境変数名と.envファイルの環境変数名が同じものがあった場合ローカルの環境変数が優先的に使われるのでローカルの環境変数と.envファイルの環境変数が被っていないか注意してください。
ローカルの環境変数を調べるコマンドは下記です。

printenv
railsのconfig/database.ymlを編集

config/database.ymlファイル .envで定義した(設定したユーザー名、パスワード、ホスト名)を設定

default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

  host: <%= ENV["DB"] %>
  username: <%= ENV["USER"] %>
  password: <%= ENV["PASSWORD"] %>

③docker-compose.ymlファイル作成する

SpeCon/
│
├── back/
│ └── spe-con/
│ ├── .env # 新しく追加された.envファイル
│ └── (Railsプロジェクトのファイル群)
│
├── front/
│ └── spe-con/
│ └── (Next.jsプロジェクトのファイル群)
│
└── docker-compose.yml # 新しく追加されたdocker-compose.yml

dcoker-compose.ymlファイルを作成する

touch docker-compose.yml

dcoker-compose.ymlファイルの中身

version: '3'

services:
  db:
    image: postgres:latest
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${DB}
      POSTGRES_USER: ${USER}
      POSTGRES_PASSWORD: ${PASSWORD}

  backend:
    build:
      context: ./back/spe-con
      dockerfile: Dockerfile.${RAILS_ENV}
    volumes:
      - ./back/spe-con:/rails
    ports:
      - "3000:3000"
      
    depends_on:
      - db
    environment:
      DATABASE_URL: postgres://${USER}:${PASSWORD}@db/${DB}
  
    tty: true
    stdin_open: true

  frontend:
    build:
      context: ./front/spe-con
      dockerfile: Dockerfile.${NEXT_ENV}
    volumes:
      - ./front/spe-con:/app
      - /app/node_modules
      
 
    ports:
      - "8080:8080"
 

volumes:
  postgres_data:
ポイント

railsとnext.jsのポート番号は3000だが同じポート番号を使用すると競合が起きてコンテナを立ち上げられないので、 next.jsのポート番号を8080に設定します.

②環境毎に動的にdockerfileを切り替える Dockerfile.${RAILS_ENV}のように環境変数を用いて,開発環境なら開発環境のdockerfileを使用できるようにしています。 docker-compose コマンドのオプションでRAILS_ENV =devみたいにすることで動的に変更する

qiita.com

④フロントエンドとバックエンドをAPI連携

前提:バックエンドとフロントエンドのAPI連携とはcorsの設定なので.
そもそもcorsとは?となった方は下記を参照ください

loveenglish.hatenablog.com

全体像
  1. gem 'rack-cors'インストール.

  2. railsでcorsの設定.

1. gem 'rack-cors'インストール.

back/spe-con/gemfile すべての環境(テスト、開発、本番)に反映

gem 'rack-cors'

gemのインストール.

bundle install
railsでcorsの設定

config/initializers/cors.rb.
下記をコメントアウトを外します

さらに少し修正.

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "http://0.0.0.0:3000/" #ここにどこのオリジンからのをリクエストを許可するか記載

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

dodckerfileを再ビルド

 docker-compose build --no-cache

コンテナの起動

 docker-compose up 

⑤. 連携ができたか確認する

完成予想図

画面に"Hello World"を表示する

全体像
  1. コンテナに入る

  2. scaffoldコマンド(データベース、コントローラー、ルーティング)実行.

  3. テストデータをデータベースに格納.

1. コンテナに入る
SpeCon/ #⇦ここのディレクトリ(docker-compose.ymlファイルがあるディレクトリ)でコンテナに入るコマンド実行
│
├── back/
│ └── spe-con/
│ ├── .env 
│ └── (Railsプロジェクトのファイル群)
│
├── front/
│ └── spe-con/
│ └── (Next.jsプロジェクトのファイル群)
│
└── docker-compose.yml

railsのコンテナに入ります

docker compose exec コンテナ名 bash

コンテナ名は

version: '3'

services:
  db:#⇦コンテナ名
省略

  backend: #⇦コンテナ名
    build:
      context: ./back/spe-con
      dockerfile: Dockerfile.${RAILS_ENV}
省略

  frontend:#⇦コンテナ名
    build:
      context: ./front/spe-con
省略

私の場合railsはbackendと名付けているので

docker compose exec backend bash

でコンテナに入る

②. scaffoldコマンド(データベース、コントローラー、ルーティング)実行.

Memoというデータベース名を作成

bundle exec rails g scaffold Memo message:string 

bundle execとはgemfile.lockに基づいてコマンドを実行するという意味

bundle exec rails db:migrate

上記でマイグレーションを実行する

③ テストデータをデータベースに格納.

db/seeds.rb

Memo.create!(
  [
{message:"Hello World"}
]
)

↓サンプルデータをデータベースに格納

bundle exec rails db:seed

page.tsx(next.js)

'use client';

import React, { useEffect, useState } from 'react';

export default function Home() {
  // サーバーから取得したデータを保存するstate
  
  const [data, setData] = useState({ message: '' });
 

  // データを取得する関数
  const fetchData = async () => {
      const response = await fetch('https://localhost:3000/memos', {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const result = await response.json();
      setData(result[0]); // データをstateに保存
  
    }

  // コンポーネントがマウントされたときにデータを取得
  useEffect(() => {
    fetchData();
  }, []);

  return (
    <div>

      {data && (
        <div>
          <h2>取得したデータ:</h2>
          <p>{data.message}</p>
        </div>
      )}
    </div>
  );
}

すると画面に"Hello World"と表示される

corsとは?

全体像

  1. cors(コーズ)とは?

  2. corsがなかった時代

  3. corsの仕組み

cors(コーズ)とは?

⇨Cross-Origin Resource Sharingの略で「別のオリジンに対してリクエストを送るのを許可する仕組み

オリジンとは?

⇨一言で言うとURLの一部 です.
具体的に言うと プロトコル + ホスト+ドメイン + ポート番号(https://www.aaa.com:443) までをオリジンと呼ぶ

なぜ必要?
  • アプリケーション内で非同期(javascript)に外部のapiサーバー(お天気やrailsなど)にリクエストを出す際に、apiサーバーにリクエストを許可してもらうために必要

  • SPA開発などでフロントエンド(reactなど)から外部のapi(お天気やrailsなど)サーバーにリクエストを送りレスポンスを返してもらえるようにしてもらうために必要

corsがなかった時代

ブログ内からjavascirptで外部のapi(楽天の商品情報など)にリクエストを送る時を考えてみる

A =ブログのコンテンツを返してくれるWebサーバー. .
B = ブログ内で紹介されている商品のapi(楽天など)サーバー.

corsがなかった時代は「同じオリジンに対してのみリクエストを送る」ことができました.
なのでAのサイト(サーバー)にアクセスするとAのサイト(サーバー)にのみjavascriptでリクエストを送ることができました. これを「同一オリジンポリシー(SOP)」という.
これ(SOP)はブラウザーの機能です
.
なのでブログ内からjavascript別のオリジン(楽天apiサーバー)にリクエストを送ることができなかった

つまりcorsとは?

別のオリジンに対してリクエストを送るのを許可する仕組み」なので.
⇨Aサイト内のjavascript(非同期処理)からBサーバーにリクエストを出してレスポンスを返してくれるようにするための設定。

そもそもなんで同一オリジンポリシー(SOP)はあるの?

CSRF(クロスサイトリクエストフォージェリ)攻撃を防ぐため(個人情報漏洩など). .

例えば もし同一オリジンポリシー(SOP)がなく別のオリジンに対して好きにリクエストが送れた場合.
もし悪意のあるサイト(https://evil.example.org)にアクセスしてレスポンスが返ってきたと同時に、悪意のあるサイト内のjavascriptが発火しapiサーバーに(https://evil.example.org)postリクエストが送られたとします。するとapiサーバーから自分の個人情報がレスポンスされてその情報が悪意のあるサイト(https://evil.example.org)に転送される可能性がある。そういった個人情報の漏洩やユーザーの意図しない挙動が勝手にできないように同一オリジンポリシー(SOP)と言うものがある。

1. corsの仕組み.

⇨corsにはCSRFといった攻撃が起こらないように別のオリジンに対して安全にリクエストやレスポンスのやり取りを行う仕組みがある.

  1. ブラウザがプリフライトリクエストをapiサーバーに送信.
    プリフライトリクエストとは?
    ⇨ブラウザがリクエストを送る前に事前にリクエストを飛ばしていいのかapiサーバーにお伺いを立てるリクエスト.

  2. apiサーバーがプリフライトリクエストを送ってきたオリジンやメソッド、ヘッダーを見てリクエストを受け取るか(信頼するか信頼しないか)を判断する。

  3. オリジンを信頼するかしないかはapiサーバーがブラウザに対してAccess-Control-Allow-XXXヘッダーを用いて伝える(レスポンス).

  4. ブラウザーは③の許可が降りた場合のみ自分のリクエストを送信する.

  5. apiサーバーは④についてのレスポンスをブラウザに返却するとともにレスポンスを参照する許可をブラウザに与える

参考情報

qiita.com www.youtube.com

www.youtube.com

サブモジュールの作成の仕方

前書き

  • サブモジュールを作成するにあたり手こずったので忘却録として記事に残します。

完成図

このように1つのリポジトリ(SpeCon)の中にfront(next.js)とback(rails)のリポジトリを混在させる

そもそもサブモジュールとは?

外部のGitリポジトリ(frontとback)を現在のリポジトリ(SpeCon)内にサブディレクトリとして追加し、独立した形で管理する仕組み。フロントエンドとバックエンドのプロジェクトを一元管理できます

www.sejuku.net

全体像

  1. githubでプロジェクトを作成する

  2. サブモジュール化と設定

  3. git pushでリモートリポジトリに変更を反映

githubでプロジェクトを作成する

今回はSpeConというプロジェクトの中にfront(next.js)とback(rails)のプロジェクトを管理しようと思います。

step1 railsのプロジェクト(back)作成

railsのプロジェクト完成画像

railsのプロジェクト作成の仕方 loveenglish.hatenablog.com

step2 next.jsのプロジェクト(front)作成

next.jsのプロジェクト完成画像

↓next.jsのプロジェクト作成の仕方

loveenglish.hatenablog.com

②サブモジュール化と設定

⇨外部のリポジトリ(frontとback)を今のリポジトリ(SpeCon)にサブモジュールとして追加するコマンド.

構文(ローカルのSpeConのルートで実行する)
git submodule add 取り込みたいリポジトリのURL

私の場合↓

git submodule add https://github.com/umeshimaru/frontend.git

今回上記のurlにはfrontend(↓)のリポジトリのURLを指定しています。 そうすると↓のようにSpeConのリポジトリの中にfrontendのリポジトリが取り込むことができて、また.gitmoduleという設定ファイルが作成されます↓

設定

その後メイン(SpeCon)のルートで下記を実行

git submodule init
git submodule update

バックエンドも同じようにやります

③ git pushでリモートリポジトリに変更を反映

その後メイン(SpeCon)のgithubにpushする

git checkout main 
git add . 
git commit -m"任意のメッセージ"
git push origin ブランチ名

サブモジュールを用いた開発の流れ

  1. サブモジュール内(frontend)のプロジェクト内でブランチを切り開発

  2. 変更をadd、commit、SPECON内のfrontendではなくオリジナル(frontend)のリモートにpushする。

  3. SpeConのルートでgit add 、commit ,pushする

next.js15のプロジェクト作成(docker化)

全体像

  1. next.jsのプロジェクト作成

  2. next.jsをdcoker化させる

next.jsのプロジェクト作成

npx create-next-app@latest

すると以下が尋ねられます

What is your project named? my-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like your code inside a `src/` directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to use Turbopack for `next dev`?  No / Yes
Would you like to customize the import alias (`@/*` by default)? No / Yes
What import alias would you like configured? @/*

どのように答えるかは人によりますが、下記が参考になりました(上記の質問は最新なので記事で聞かれている質問と少し差異があるかもですがご了承ください)

zenn.dev

next.jsをdcoker化させる

開発環境のDockerfile.dev
FROM node:23-alpine
WORKDIR /app
COPY package*.json ./
COPY certificates /app/certificates
RUN npm install
EXPOSE 8080
CMD ["npm", "run", "dev"]
プチ解説
  • FROM.
    ⇨イメージの取得.
    下記のdockerhubでイメージを取得してきます(今回はnodeの23-alpineというバージョン)。↓.
    https://hub.docker.com/_/node

どのバージョンを選べばいいの? 自分のPCのCPUのアーキテクチャ(ARMかintel)に対応したものを選ぶ

macには2種類のCPUのアーキテクチャがあるみたいです↓ ] ⇨下記のコマンドで自分のPCがARMかintelのどちらかわかります

uname -m
x86_64          # → Intel
$ uname -m
arm64           # → ARM

詳しくは↓

zenn.dev

私はarm64のARMだったので、arm64が対応しているかつ軽量で有名なalpineのバージョンを選びました

  • WORKDIR
    ⇨この後COPYコマンドなどを実行する際に必要なので作業場所を作成(ディレクトリ)。

  • COPY ⇨ローカルPCのnext.jsのプロジェクトをコンテナ内にコピーするためにこのコマンドを使う (COPY . .)

  • RUN ⇨イメージ作成時に実行されるコマンド

  • CMD.
    ⇨コンテナ起動時に毎回実行されるコマンド CMDは1つのdockerfileに1回記述して、RUNは何回でも記載できる。

本番環境のDockerfile.pro
FROM node:23-alpine

WORKDIR /app

COPY . .

RUN npm install
RUN npm run build

CMD ["npm", "start"]

rails8のプロジェクトの作成(docker化)

Rails全体像
  1. rails-newコマンドインストール.

  2. railsのプロジェクト作成

  3. railsをdcoker化させる

rails-newコマンドインストール.

事前にrails-newコマンドを使いますのでインストールがまだの方は下記からインストールお願いします.

rails-newコマンドのダウンロード場所↓ github.com

M1 macOS の方は↓の形式のファイルをダウンロードしてください

 rails-new-aarch64-apple-darwin.tar.gz

rails-newがインストールできたか確認

rails-new --help

インストールに成功すると↓のようにコマンドの使い方が表示されます

Usage: rails-new [OPTIONS] <ARGS>...
       rails-new [OPTIONS] [ARGS]... <COMMAND>

Commands:
  rails-help  Print `rails new --help`
  help        Print this message or the help of the given subcommand(s)

Arguments:
  <ARGS>...  arguments passed to `rails new`

Options:
  -u, --ruby-version <RUBY_VERSION>    [default: latest]
  -r, --rails-version <RAILS_VERSION>  
      --rebuild                        
  -h, --help                           Print help
  -V, --version                        Print version

railsのプロジェクト作成

構文

rails-new [OPTIONS] <ARGS>
 rails-new -u 3.4.1 -r 8.0.1 spe-con --api --database=postgresql --devcontainer --skip-test
プチ解説
  • -u 3.4.1 -r 8.0.1.
    rubyrailsのバージョンを指定しています。

  • spe-con.
    ⇨プロジェクト名(ここは各自好みの名前で設定してください)

  • --api --database=postgresql --devcontainer --skip-test.
    apiモードでデータベースはポスグレを指定。--devcontainerとはコンテナを使って環境構築するという意味です。デフォルトのテストファイルはプロジェクトに含めないでという意味です

rails-new(新しい)とrails new(古い)の違い。

要は「railsプロジェクトを始める前に事前にrubyとかPCになくてもいいよ」ってことです。

詳しくは↓ blog.tomoyayoshida.com

railsをdocker化

今回開発環境と本番環境でインストールgemなどが違うためdcokerfileを環境ごとに分けています。

開発環境のDockerfile(今回ファイル名をわかりやすいようにDockerfile.devとします).

Dcokerfile.dev

FROM ruby:3.4.1

# 必要なパッケージのインストール
RUN apt-get update -qq && \
    apt-get install -y build-essential libpq-dev nodejs

# 作業ディレクトリの設定
WORKDIR /rails

# GemfileとGemfile.lockのコピー
COPY Gemfile Gemfile.lock ./

# Bundlerを使用して依存関係をインストール
RUN bundle install

# アプリケーションコードのコピー
COPY . .

CMD ["bash", "-c", "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b 'ssl://0.0.0.0:3000?key=./localhost-key.pem&cert=./localhost.pem'"]
本番環境のDockerfile(今回ファイル名をわかりやすいようにDockerfile.proとします)*こちらはrails-newした時に作成されるデフォルトの本番環境用のdockerfileです.

Dockerfile.pro

# syntax=docker/dockerfile:1
# check=error=true

# This Dockerfile is designed for production, not development. Use with Kamal or build'n'run by hand:
# docker build -t spe_con .
# docker run -d -p 80:80 -e RAILS_MASTER_KEY=<value from config/master.key> --name spe_con spe_con

# For a containerized dev environment, see Dev Containers: https://guides.rubyonrails.org/getting_started_with_devcontainer.html

# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base

# Rails app lives here
WORKDIR /rails

# Install base packages
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y curl libjemalloc2 libvips postgresql-client && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Set production environment
ENV RAILS_ENV="production" \
    BUNDLE_DEPLOYMENT="1" \
    BUNDLE_PATH="/usr/local/bundle" \
    BUNDLE_WITHOUT="development"

# Throw-away build stage to reduce size of final image
FROM base AS build

# Install packages needed to build gems
RUN apt-get update -qq && \
    apt-get install --no-install-recommends -y build-essential git libpq-dev pkg-config && \
    rm -rf /var/lib/apt/lists /var/cache/apt/archives

# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
    rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
    bundle exec bootsnap precompile --gemfile

# Copy application code
COPY . .

# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/




# Final stage for app image
FROM base

# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails

# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
    useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
    chown -R rails:rails db log storage tmp
USER 1000:1000

# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]

# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]