チームでの開発経験を積んだ

いつもお世話になっているFBCというプログラミングスクールで実際に運用されているWebアプリの開発にチーム開発メンバーの一員として参加しました。 入会・休会・退会に関する処理、日報の作成や提出、質問の投稿や回答、イベントのお知らせや申し込みなど、様々な機能がある中でフロントエンドからバックエンドまで割と幅広く関わらせていただきました。

こちらの記事にもあるとおり(【新人プログラマ応援】学習用のプログラムと仕事で書くプログラムは何が違うか)、実際にアジャイル開発を採用している会社で働いたことはありませんが、実際のお仕事と同じ様な形式でした。

特に印象的だったのは、GitHubでもお馴染みの草(=コントリビューション)に似た機能などをVue.jsからReactへ移行するissueです。

FBCのチーム開発の前提
  • 1スプリントの長さ = 1週間
  • 見積もりの基準になるストーリー = Wikiにページネーションを実装する
    • Wikiにページネーションを実装する」にかかる時間がベロシティ(チームがどれだけの速度で進むことができるかを表す数値)1ポイント。
    • Wikiにページネーションを実装する」の5倍時間がかかるストーリーはベロシティ5ポイント。
スプリントの流れ
  1. デモ
  2. 今スプリントの振り返りMTG ※一般的にはリリース後
  3. 次スプリントの計画MTG
  4. リリース(木曜日)

毎週水曜日には今週行った作業を画面共有してみんなに向けてデモをしました。自分が何をやったかを報告するだけでなく、やれなかったこと、できなかったこと、困っていることを報告しました。続いて、今週の作業のふりかえりを行い、苦労したこと、上手くいかなかったこと、上手くいったこと、相談したいことを話しました。最後に次の一週間で各自がやることを決め、そのやること(Issue)にどれくらいの労力がかかるかのポイント付けをされる形でした。

チーム開発ではrebase方式を採用(以下のような理由から、OSS の場合は特に rebase 方式を取ることが多い)
  • メリット

    • 三者の目からコミットログを見たときにはどのように機能が追加されていったかが目で追いやすくなる。
    • コードを書いた本人にとってはあると嬉しい情報が消えてしまう代わりに、開発者全体や、直接の開発者以外の人から見たらわかりやすいコミットログになる。
    • mergeコミットが作られない。
  • デメリット

    • ある意味歴史の改ざんのため、過去にもらったレビューの指摘箇所が消えてしまうということが起こる。
心がけていたこと
  • issueに着手する前に自分が実装する上で考えていることだったり、不安や疑問に思うことをメンターをはじめチームメンバーとすぐに会話することを心がけていました。テキストコミュニケーションで伝えづらいところを解消すべく、ペアプロを実施いただけるよう積極的に声をかけていました。案の定、「聞いといてよかった〜」と感じる場面がとても多かったです。
  • 作業前に洗い出しておく要件や実装前のメモ、作業中に疑問に感じたこと、作業後の振り返りなどは、Notionで管理し、週一回のMTGですぐに報告できるように管理してました。Discordもスクールで使用していましたが、日報にわからないことだったり苦労していることをまとめていました。実務ではチャットも頻繁に見ていただく機会が多いと思うので(会社によりますが)、チャットツールに合わせて作業ログとして呟いていきたいと考えています。
  • 当たり前ですが、主体性を持って取り組むということも心がけていました。基本的に、「自分は〇〇だと考えるのですが...」から会話がスタートするように自分の意見を盛り込んでました。
  • プルリクエストのdescriptionには、issueに対するプルリクエストの目的や確認観点などを詳細かつ簡潔に伝えられるように記載していました。
まとめ

チームメンバーやメンターの方とのペアプロはプログラムを書くこと以外にも開発効率を上げる小技や実装する上での考え方を学べたりと、とにかく学びだらけでした。

途中で仕様の変更があったり、プラスで考えなきゃいけないことが出てきたりと、苦しい局面もありましたが、 総じてやっぱり開発は楽しいと感じることができてよかったです✨

いわゆるウォーターフォールモデルを経験してきた自分としてはアジャイルな開発手法の一つであるスクラムを経験できたのはとても新鮮でした。 (ウォーターフォール開発については定説な定義が定まっていないようです。 参考:「ウォーターフォールモデルの起源に関する考察 ウォーターフォールに関する誤解を解く」

アジャイル開発を学ぶ上で、より価値があると宣言されている以下の4つの価値観にはとても感銘を受けました。

  • プロセスやツールよりも個人と対話を
  • ドキュメントよりも動くソフトウェアを
  • 契約交渉よりも顧客との協調を
  • 計画に従うことよりも変化への対応を

※ プロセスやツール、ドキュメントなどをおろそかにしてよい。ということではなく、価値のある必要なドキュメントは作成するということです。

最後に、このチーム開発は1つのプラクティスでしたので、終了条件である「issueに割り振られたポイント20ポイント分のプルリクエストが全てマージ」が満たされたので、取り組んだ内容をここにまとめておきます。

Pull Request

Point Issue PR
1 Q&Aの削除確認メッセージが受講生向けになっている #6932
1 「8時間後に5日経過」よりも「8時間以内に5日経過」の方がよいかも #6943
1 研修生の相談部屋個別ページに企業ロゴを表示したい。 #6959
1 【メンター向け】「はじめての日報です!」の通知メールのリンクがおかしい #6974
2 相談部屋の連絡通知がメールをオフにしても管理者にメールで届く。 #6981
1 slim-lint 対応-5(いくつかのコードに対してスタイル違反を報告していたため5ファイル分修正) #7000
2 定期イベントの主催者が退会、休会などして主催者がいなくなってしまった場合は、管理者が主催者になるようにしたい。 #7031
3 grass.vueをreactに対応させる #7092
5 generation-users.vueをreactに対応させる #7170
3 分報チャンネルを自動で作成する際にユーザーの分報URLも自動で登録されてほしい #7564
1 他のユーザーがWIPの提出物を閲覧したときのページタイトルの不一致 #7147

Review

Point Issue PR
2 提出物が更新された際に、どの提出物が更新されたかがわかる通知に変更した #6933
1 User の discord_account 属性 と DiscordProfile の ccount_name 属性 が混在しているので、discord_account 属性 を削除したい。 #6945
usersテーブルから不要になったDiscord関連のカラムを削除する #6945
1 ラクティスのページの title タグは、最初にプラクティスと表示したい。 #7004
1 Q&A個別ページのtitleタグは、最初に Q&Aと表示したい。 #7044
1 Talksテーブルのunrepliedカラムを削除する #7064
1 日報の学習日入力部分に範囲バリデーションを追加する #7119
3 コースごとの参考書籍一覧が欲しい #7152
5 imagemagickからlibvpsに移行したい #7330
3 相談部屋一覧を非vue化したい #7447
2 [退会]休会から三ヶ月後に自動で退会されるようにしてほしい。 #7515
2 [退会]休会中に三ヶ月後に退会にしないフラグが欲しい。 #7515
3 [分報]退会したら自動で分報が削除されてほしい #7515
3 view_componentを導入する #7674

Kaigi on Rails 2023 に参加してきました!

Kaigi on Rails 2023に2日間参加してきましたー!

RubyKaigi 2022に参加して以来のカンファレンスへの参加で、オフラインでの参加は今回が初めてでした。
Kaigi on Railsは今回が初めてのオフライン開催だったので、記念すべき日に立ち会えたことをとても光栄に思います。

自分が喋るわけでもないのにドキドキが止まらないままスタート...!

Railsを本格的に学び出して1年以上経ちましたが、まだまだ分からないことだらけで...
そんな初学者の自分でも頷ける内容だったものをピックアップして書き留めておきたいと思います。

Day1 -10.27

speakerdeck.com

馴染み深いHTTPリクエストを題材にお話を聞くことができました。
業務やプライベートにおいて、よく分からないけど、とりあえず実装できているというケースは自分も多々あるなと...
自分の理解を深める行動として、最小の動くサンプルは作成していきたいと考えさせられる内容でした。

speakerdeck.com

ActiveRecordRailsエンジニアの方で知らない方はいないでしょう。
ですが、その内部構造について知る機会が自分にはまだなかったです。
DBはMySQLでバックトレースを元にしたお話で、とても解像度の上がるお話をいただけました。 ActiveRecordの箇所で詰まったら見直していきたいです。

speakerdeck.com

自分自身、今は某開発現場にいます。
このお話であった、「推測は必要だけど推測したら計測せよ、計測してダメなら思いついたことをどんどん試していく、コードにしたものとコードにしなかったことがプログラミング」というのは、具体例も含めてとても共感の持てる内容でした。

1日目は全体を通してAWSとかインフラ周りが絡んだお話が多かったと感じました。
AWSはまだまだ知識が浅いので、再度学習し理解を深めていきたいと感じました👀

Day2 -10.28

2日目も受付開始とともに入場しましたw

speakerdeck.com

こちらトークとスライドもとても印象深いですがw
目の前でモブプロの実践をしていただき、実践前にタスクを1つ1つ洗い出していたのがとても印象的でした。 これは自分もやってる時とやらない時がある状況なので、どんなに簡単なタスクでも実践していかないといけないと痛感しました。
また、業務ではやってもペアプロまでで、そこまで頻繁に行われるわけではなく、ほぼソロプロです。 チームや会社の方針にもよるのでしょうけれど、実践して損はないのでは?と考えさせられる発表でした。 Railsエンジニアに転向した暁にはどんどん試していきたいです。

Hotwireを使って管理画面を簡単にプチSPA化する」(資料なし)

こちらの講演だけではなく、今回の講演では全然知識がないHotwireに関する話題も多かったです。 ただ、お話を聞いていくうちに現在作成に取り掛かろうとしている自作サービスのモーダルでも活かせる?!ということを拾うことができました。

まとめ

FBC関係者の皆さんと)

ランチやアフターでは自分が所属するFBCの受講生、卒業生、メンター、その他企業のたくさんの方々とお話しすることができ、とても嬉しかったです😄
あと、FBC卒業生の登壇の数が半端なくて凄すぎますw尊敬しかないです!

2日間、有意義なひと時を過ごすことができました。 気持ちを切り替えて、まずはRailsエンジニアに転向するという目標の達成に向けて教えていただいたことを念頭に学習していきます💪

そして、人ごとではなく真剣にプロポーザルを通して、Speakerになれるよう経験を積んでいきたいです。

とにかく最高な2日間でした! 運営の皆さんも貴重な場を提供してくださりありがとうございました。(遠くて見にくいですが🙏)

目指すエンジニア像

さらに追加/修正を行い、ブラッシュアップしていきますが、現時点での目指すエンジニア像をアウトプットをしておきます。

1〜3年後のエンジニア像

職種:バックエンドエンジニア

  • Webアプリのパフォーマンスと安定性を向上させることを目指す。
  • DBへのアクセスをスムーズにするためのキャッシュの利用や、APIの設計と実装に関するスキルアップを通じて、 UXの向上に貢献する。
  • 加えて、PCの基本的な構造や仕組み、例えばOSやCPU、データ構造、アルゴリズム、ネットワークについての知識を深め、それらの要点を簡潔に説明できる能力を身につけたい。(低レイヤーの知識を持つことは、大・中規模のプロジェクトに関わる際に、より高度なバックエンドの技術的な判断を下すのに役立つと考えているため)
  • 英語ドキュメントなどを翻訳なしで読めるようにする。
Why?
  • システムの裏側のロジック構築に魅力を感じ、特にシステムの基盤を担当することに興味を持っているため。
  • 一つのサービスを構築する過程で、バックエンドでは言語や技術の選択肢が非常に幅広く、これにより様々な知見(Web/Apサーバ、HTTPプロトコルAPI、認証/認可、セキュリティなど)を得ることができる点に喜びを感じるから。
  • 加えて、大規模なサービスの開発においてバックエンドの役割の重要性が増してくると感じており、将来的にはCTOなどのリーダーシップを持つ役職を目指す中で、この経験は非常に価値あるものと考えたから。
  • また、バックエンドの知識を持つことで、フロントエンドやインフラとの兼務もスムーズに行えると考えているため。

5年後のエンジニア像

職種:テックリード or PM

  • 小規模サービスに携わることで、システムの全体構築から運用までの経験を積みたい。そして、バックエンド領域以外の知見を広げ、システム全体像を深く理解したい。 この経験を元に、チームを技術面でリードし、より効果的なプロジェクトの実現に向けて開発プロセスを構築・実行する能力を磨きたい。
Why?

CTO、延いてはCIOのキャリアパスを考えているため、チーム全体の技術面でのリーダーシップを培いたいから。

10年後のエンジニア像

職種:CTO、延いてはCIO

  • 今までのエンジニアリングの専門的知識と経験を活かしたい。ドメイン(事業領域)や経営戦略を理解し、技術とビジネスの両方で事業の拡大に貢献したい。
Why?

事業、会社を成長させることへの関心が強いから。

SWRを使ってAPIをコールする

今回はSWRの簡単な使い方を習得しました。
Fetchで取得したデータを表示するサンプルアプリにSWRを導入して、UI/UXを改善しました。この改善で、APIからのレスポンスを待つ間に画面が真っ白になる問題を解消しました。

【参考】
SWR公式ドキュメント

修正する内容

  • APIコールが失敗した場合、Failed to load.と表示する。
  • APIのレスポンスが返ってくるまでの間、Loading...と表示する。
  • APIのレスポンスが返ってきたら、現状と同じくStatus: OKと表示する。
  • コール先のURLやHTTPヘッダは変更しない。

なぜSWRを使うのか

SWR(Stale-While-Revalidate)は、React用のデータフェッチライブラリで、いくつかの重要な利点がある。

  1. SWRは背後でデータの有効性を確認しつつ、クライアントに直ちにデータを提供。ユーザーはページの読み込みを待つ時間が少なく、それでいて最新のデータを見ることができる。
  2. APIからエラーレスポンスが返された場合、エラー状態を明確に捉えて対処できる。具体的なエラーメッセージを表示するなど、UXの向上に寄与する。
  3. APIからのレスポンスが未返信の場合、Loading...といった状態表示が容易。ユーザーに進行状況を明確に示せる。
  4. データが変更される可能性がある場合に自動で再取得を行う設定が可能。ユーザーが手動でページを更新する必要がなくなる。
  5. 一度取得したデータをキャッシュし、同じAPIエンドポイントに対する後続のリクエストで再利用する。ネットワークリクエストの数が減り、アプリのレスポンスが速くなる。

修正前

  • fbc-swr-practice/src/App.jsx
import { useState } from "react";
import "./App.css";

function App() {
// 使用するAPIのURL。
  const url = "https://httpstat.us/200?sleep=2000";
// クライアントが理解できるコンテンツタイプをサーバーに伝えるもの。
  const headers = { Accept: "application/json" };

  const [status, setStatus] = useState("");

  fetch(url, { headers })
    .then((res) => res.json())
    .then((json) => setStatus(json.description));

  return <>{status && <p>Status : {status}</p>}</>;
}

export default App;

実装

1.SWRライブラリの追加

プロジェクトのルートディレクトリでSWRライブラリをインストール。

$ npm install swr
修正後
  • fbc-swr-practice/src/App.jsx
import "./App.css";
import useSWR from 'swr';

function App() {
  const url = "https://httpstat.us/200?sleep=2000";
  const headers = { Accept: "application/json" };

  const fetcher = url => fetch(url, { headers }).then(res => res.json());

  const { data, error } = useSWR(url, fetcher);

// エラーが存在する場合、エラーメッセージを表示。
  if (error) return <p>Failed to load.</p>;
// データがnullまたはundefinedの場合、ローディングメッセージを表示。
  if (!data) return <p>Loading...</p>;

// データがロードされた場合、状態を表示。
  return <p>Status: {data.description}</p>
}

export default App;
  • useSWRフックを使用しているが、このフックはデータがまだロードされていない(datanullまたはundefined)場合や、エラー(errortrue)が発生した場合に、それぞれ適切なメッセージを表示。

まとめ✏️

SWRの知識を活かして、更にリッチなWebアプリを作っていくのが楽しみです。特に、リアルタイムなデータ更新やキャッシュの活用といったところを、もっと深掘りしていきたいなと感じました。SWRの高度な設定や他のReact Hooksとの組み合わせとかを試してみるのも面白そう。

Reactでグローバルな状態管理を行う方法(Context APIの活用)

以前作成したメモアプリに「ログインボタン」を実装し、グローバルなstateを扱う最もシンプルな方法として、React の機能であるContext(コンテクスト)を学びました。

knkkojt6.hatenablog.com

概説

useState()によって管理される値はローカルなstateと呼ばれ、その値は特定のコンポーネント内でしか利用できない。 しかし、複数のコンポーネントからその値を参照したい場合、propsを大量に渡す必要が出てくる。このような状況を避けるために、グローバルな状態管理が有用。

Contextとは

コンポーネントツリー全体でデータを共有するための方法。
複数のコンポーネントにprops経由で渡す手間が省ける。
アプリケーション全体で必要とされる情報(認証情報、テーマ設定など)を扱う際に便利。

Contextの作成

以下のようにcreateContext()を使用してContextを作成。

import { createContext } from 'react';

export const MyContext = createContext(defaultValue);
Providerの使用

Providerは、value属性を用いて子孫コンポーネントにコンテクストの値を供給。 一方で、子孫コンポーネントuseContext()フックを使用してこの値にアクセスできる。

<MyContext.Provider value={/* some value */}>
  {/* children */}
</MyContext.Provider>

今回の要件

  • 未ログイン時はログインボタンを、ログイン時はログアウトボタンを表示する。
  • 未ログイン時はメモの閲覧のみ可能で、ログインするとメモの追加/更新/削除ができる。

※ UI制御の練習のため、ID/PasswordやOAuth等を利用した実際の認証機能を実装はなし。
ログインボタンを押せばログイン状態になり、ログアウトボタンを押せばログアウト状態となるような「見かけ上のログイン機能」。新規アカウント作成等の機能もなし。

実装

可読性と保守性を高めるために、ReactのJSX構文を含むファイルは.jsxであるべき。

1. ログイン状態管理用のContextとカスタムフックを作成

新規でsrc/AuthContext.jsxを作成し、そこにログイン状態を管理するためのAuthContextを定義。
ここでは、Custom HookのuseLoginも作成。

import { createContext, useContext, useState } from "react";

const AuthContext = createContext();

export function AuthProvider({ children }) {
  const [loggedIn, setLoggedIn] = useState(false);

  const login = () => setLoggedIn(true);
  const logout = () => setLoggedIn(false);

  return (
    <AuthContext.Provider value={{ loggedIn, login, logout }}>
      {children}
    </AuthContext.Provider>
  );
}

export function useLogin() {
  return useContext(AuthContext);
}
2. App.jsでログインプロバイダーを使用

src/App.jsで作成したAuthProviderをラップすることで、全コンポーネントでログイン状態を共有できるようにする。

import { AuthProvider } from "./AuthContext.jsx"; // 追加
...
function App() {
  return (
    <AuthProvider>
      {/* ラップする */}
    </AuthProvider>
  );
}

export default App;
3. ログイン/ログアウトボタンの実装

ログイン/ログアウトボタンを追加。

src/components/MemoList.jsx

import { useLogin } from "../AuthContext.jsx"; // 追加

function MemoList({ memos, onMemoSelect, onMemoAdd }) {
  const { loggedIn, login, logout } = useLogin(); // 追加

  return (
    <>
      <div className="login-button">
        <button onClick={loggedIn ? logout : login}>
          {loggedIn ? "ログアウト" : "ログイン"}
        </button>
      </div>
      {memos.map((memo) => (
        <div
          key={memo.id}
          onClick={() => onMemoSelect(memo.id)}
          className="memo-item"
        >
          {memo.text.split("\n")[0]}
        </div>
      ))}
      {loggedIn && <button onClick={onMemoAdd}>+</button>} {/* ログイン時のみ表示 */}
    </>
  );
}

export default MemoList;
4. ログイン時のみ特定のUIを表示

メモの編集と削除をログイン状態に応じて制御。

src/components/MemoDetail.jsx

import { useLogin } from '../AuthContext'; // 追加

function MemoDetail({ memo, onMemoSave, onMemoDelete }) {
  const { loggedIn } = useLogin(); // 追加
  ...

  return (
    <>
      <textarea
        value={memoText}
        onChange={(e) => setMemoText(e.target.value)}
        readOnly={!loggedIn} // ログインしていない場合は読み取り専用
      />
      {loggedIn && (
        <>
          <button onClick={handleSave}>編集</button>
          <button className="delete-button" onClick={onMemoDelete}>
            削除
          </button>
        </>
      )}
    </>
  );
}

export default MemoDetail;

まとめ✏️

今回は見かけ上でしたが、ログイン/ログアウト機能を割と簡単に実装できました。 Contextでアプリ全体で状態を共有できるので、コードの冗長性を大幅に削減することができました。 次のステップとしては、実際の認証機能(JWT, OAuthなど)や新規アカウント作成機能も踏まえて学習していきたいです。

参考資料
コンテクストで深くデータを受け渡す
useContext
createContext
React Context を export するのはアンチパターンではないかと考える
カスタムフックで Context をエクスポートしないようリファクタリングする

ReactでSPAを作る

今回は、Reactでメモアプリ(SPA版)を作成しました。
このアプリはCRUD(一覧表示、詳細閲覧、追加、編集、削除)を実装していて、ページ全体のリロードなしでメモの閲覧、追加、編集、削除が可能になっています。
アロー関数とfunctionが混在していますが、コンポーネントの部分と、イベントハンドラなどの小さい関数を区別するために、コンポーネントの定義にfunction、短いコールバック処理にはアロー関数を使いました。

前提

SPA(Single Page Application)について
  • 1つのHTMLページで完結し、ページ遷移を伴わずにユーザーとの対話が可能なウェブアプリケーションのこと。
  • ページ遷移がないため、スムーズなユーザー体験を提供できる。

公式ドキュメントに書かれているように、Reactを使って1からSPAを開発したい場合、Next.jsやRemixなどのフレームワークを利用することが推奨されている。
今回はReactのみ(フレームワークなし)でプロジェクトを始めることのできるツールであるCreate React Appを使用。

要件

  1. 一覧
    • メモの1行目をタイトルとして一覧表示する。タイトルをクリックするとそのメモの編集状態に移行する。
  2. 詳細
    • 編集状態=詳細
  3. 追加
    • をクリックすると「新規メモ」というメモファイルが作成され、編集状態に移行する。
  4. 編集
    • テキストエリアにメモの内容を表示し、編集できる。編集ボタンをクリックすると保存される。
  5. 削除
    • 編集状態で削除ボタンをクリックするとメモは削除される。そのあとは一覧の状態に移行する。
  6. 保存先ストレージ
    • LocalStorageやFirebaseなど、永続化できれば自由。

アプリの構造

  • 不要なファイルを削ぎ落として、以下のような形に。
memo-app/
├── public/
│   └── index.html
└── src/
    ├── App.js // すべてのメモデータの管理。メモの追加、編集、削除の操作。選択されたメモの管理。
    ├── components/
    │   ├── MemoDetail.js // 選択されたメモの詳細表示。メモの編集と削除。
    │   └── MemoList.js // メモの一覧表示。タイトルクリックで編集モードへの遷移。新規メモの追加。
    ├── hooks/
    │   └── useLocalStorage.js // LocalStorageを用いた永続化の実装。
    ├── index.js // アプリケーションのエントリーポイント。
    └── styles.css
├── .eslintrc.json
├── .gitignore
├── package-lock.json
├── package.json

処理の流れ

1. アプリの起動 (index.js)
  • ReactアプリがDOMにマウントされる。
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
2. メモの一覧表示(App.js, MemoList.js)
<MemoList
    memos={memos}
    onMemoSelect={handleMemoSelect}
    onMemoAdd={handleMemoAdd}
/>
function MemoList({ memos, onMemoSelect, onMemoAdd }) {
  return (
    <>
      {memos.map((memo) => (
        <div
          key={memo.id}
          onClick={() => onMemoSelect(memo.id)}
          className="memo-item"
        >
          {memo.text.split("\n")[0]}
        </div>
      ))}
      <button onClick={onMemoAdd}>+</button>
    </>
  );
}

export default MemoList;
3. 新規メモの追加 (App.js)
  • handleMemoAdd関数で新しいメモが作成され、追加される。
const handleMemoAdd = () => {
  const newMemo = { id: memos.length, text: "新規メモ" };
  setMemos([...memos, newMemo]);
  setSelectedMemoIndex(memos.length);
};
4. メモの選択と詳細表示 (App.js, MemoDetail.js)
<MemoDetail
    memo={memos[selectedMemoIndex]}
    onMemoSave={handleMemoSave}
    onMemoDelete={handleMemoDelete}
/>
5. メモの編集と保存 (MemoDetail.js)
const handleSave = () => {
  onMemoSave({ ...memo, text: memoText });
};
6. メモの削除 (App.js)
  • handleMemoDelete関数でメモが削除される。
const handleMemoDelete = () => {
  const updateMemos = [...memos];
  updateMemos.splice(selectedMemoIndex, 1);
  setMemos(updateMemos);
  setSelectedMemoIndex(null);
};
7. ローカルストレージの利用 (useLocalStorage.js)
  • カスタムフックuseLocalStorageでローカルストレージへの保存と読み込みが行われる。

重要な箇所と使用技術

  • 状態管理にuseStateが使用し、各コンポーネントの内部状態を管理。
  • LocalStorage(useLocalStorageカスタムフック)を使用し、メモを永続化。
  • MemoList.jsの中で、map関数が使用されメモの一覧を表示。
    key属性が重要で、keyindexにしてしまうと、メモを削除したときに同じkeyの値が別のメモに使われることになる。
    これはユニークなIDを使用して回避。

まとめ✏️

最初は「SPAとは??」「巷で噂になってるけど実際どうなんだろう?」と思っていましたが、開発を進める中でページ遷移なしでスムーズなUXを提供できる凄さを実感することができました。
コンポーネント間の状態管理等を学びましたが、ユーザーがどう使うか、どんな体験を感じるかを常に考慮しなければいけないことに気づくことができました。Next.jsも学習するぞ〜

CLI版メモアプリを作成(Node.js)

今回は、Node.jsでメモの追加・一覧表示・参照・削除ができるメモアプリ(CLI版)を作成しました。

データの保存先にはsqlite3、JavaScriptのクラス構文を使って作成しました。

要件

1. メモの追加

標準入力に入ってきたテキストを新しいメモとして追加する。

$ echo 'メモの内容' | app.js
2. メモの一覧を表示

それぞれのメモの最初の行のみを表示する。

$ memo.js -l
メモ1
今日の日記
晩ご飯のレシピ
3. メモの参照

選択したメモの全文が表示される。
(カーソルを合わせた時点もしくはメモを選択した時にメモの全文が表示される)

$ memo.js -r
Choose a note you want to see:
  メモ1
  今日の日記
> 晩ご飯のレシピ

晩ご飯のレシピ
カレー
豚肉
じゃがいも
人参
タマネギ
カレールー
4. メモの削除

選択したメモが削除される。

$ memo.js -d
Choose a note you want to delete:
  メモ1
  今日の日記
> 晩ご飯のレシピ

必要なパッケージをインストール

$ npm install --save enquirer minimist sqlite3
  • enquirer: ユーザからの入力を受け取るためのライブラリ。
  • minimist: コマンドライン引数を解析するライブラリ。
  • sqlite3: SQLite3データベースと連携するためのライブラリ。

memoDB.js

  • SQLite3データベースとのインタラクション(相互作用)を処理するクラス。
  • serialize()
    • 指定したテーブルが存在しない場合は、新しいテーブルを作成。
      テーブルは、自動的に増加する主キーのidとテキストデータを保持するtextの2つのカラムを持っている。

memo.js

  • メモ操作に関連するクラス。
    MemoDBクラスのインスタンスを引数として受け取り、Memoクラス内でデータベースの操作を行う。

menu.js

  • メモ選択のメニューを表示するクラス。
  • コンストラクタにchoices(選択肢)を引数として渡す。(メモの配列を表す)
    配列の各要素に対して、最初の行を名前(name)、idを値(value)とするオブジェクトを作成し、その結果を新しい配列として保存。

  • chooseMemoId()

    • ユーザーにメモを選択させるためにprompt関数を使用。
    • promptはenquirerライブラリを使用し、メモの選択メニューを表示し、ユーザーからの選択を受け取る。以下のキーを持たせる。
      • typeは入力の種類を指定し、ここでは"select"を指定。
      • nameはプロンプトの名前を指定。
      • messageはプロンプトのメッセージを指定。
      • choicesには先程作成したchoices配列を指定。
      • result関数では、選択した選択肢の値(この場合、メモのID)を返す。

cli.js

  • main関数を定義
    • MemoDBMemoクラスのインスタンスを作成し、minimistコマンドライン引数をパース。
    • process.argv.slice(2)は最初の2つの引数(nodeのパスと実行ファイルのパス)を除いた残りの引数を取得。
  • process.stdin.isTTYは、現在のプロセスがTTY(端末)からの入力を受け取っているかどうかをチェック。否定演算子(!)を前に付けているため、このif文はTTYからの入力がない場合、つまりパイプやリダイレクションからの入力がある場合に実行される。その入力を読み取り、新しいメモを作成。作成が完了したら、その内容をコンソールに表示。
  • 最後に、標準入力からデータを非同期に読み込むreadStdin関数と、メインの関数を実行するmain()を定義。readStdin関数はPromiseを返すため、データが全て読み込まれるまで待機し、その後処理を進めることができる。

JavaScriptのclass構文について

1. そもそもクラスとは?
  • プログラミングにおける「クラス」とは、オブジェクトを作るための設計図のようなもの。
    オブジェクト指向プログラミングでは、クラスを使ってデータとそれを操作するメソッド(関数)を一緒にまとめる。
2. JavaScriptのクラスはシンタックスシュガー
  • JavaScriptにおいても、クラスはデータとメソッドを一緒にまとめる役割を果たす。
  • JavaScriptのクラスは、他の言語のクラス構造と同じように見えるが、内部的には「プロトタイプベースのオブジェクト指向」を使用。
    • 「プロトタイプベース」...オブジェクトが他のオブジェクトを元にして作られることを意味する。これはクラスベースの言語と異なり、クラスを必要としないのが特徴。クラス構文はこのプロトタイプベースのオブジェクト指向を扱いやすくするための糖衣構文。(シンタックスシュガー

まとめ✏️

  • 非同期処理は、一部の時間のかかる操作(通常はI/O操作、たとえばデータベースへのクエリ、ファイルの読み書き、ネットワークリクエストなど)をバックグラウンドで実行し、それが完了するのを待つことなく他の操作を続けるために使用。

    • 上記のソースコードでは、データベースの操作(メモの作成、表示、削除など)と標準入力からの読み取りが非同期処理として行われている。こういった操作は、それぞれ時間がかかる可能性があるため、非同期処理を行う。特にデータベースの操作は、データベースエンジンが操作を完了するまでに時間がかかる場合がある。

    • 非同期処理を行うことで、アプリケーションは操作が完了するのを待つことなく、他の処理を行うことができる。これにより、アプリのパフォーマンスが向上、レスポンス時間が短縮され、ユーザーエクスペリエンスが向上。

    • 上記のソースコードの場合、たとえばメモの作成や表示を行うときに、それが完了するまで他の操作(たとえば別のメモの表示や削除)がブロックされることはない。これは非同期処理のおかげ。

【参考】
クラス - JavaScript | MDN
クラス(Class) - とほほのWWW入門
Node.jsでのCLIの作り方と便利なライブラリまとめ - Qiita
mapbox/node-sqlite3: Asynchronous, non-blocking SQLite3 bindings for Node.js