6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js × Laravel チーム開発! - Vision Board 作成アプリ

Last updated at Posted at 2024-02-07

1. はじめに

私は2023年10月より、内定直結型エンジニア学習プログラム「アプレンティス」に2期生として参加しています。

【働きながら学べる】エンジニア実習 アプレンティス

その中で取り組んだ二度目のチーム開発が先日発表会を迎えました!
一連の開発過程と、完成したアプリケーションをご紹介しつつ、良かった点、反省点を振り返り、学びを整理しようと思います。

チーム開発未経験の駆け出しエンジニアの方が、チーム開発のイメージを掴む助けになれば幸いです。
長文になっているので、目次を参考に、必要な箇所を読んでいただけたらと思います。

目次

1.はじめに
2.メンバー構成
3.チーム開発 week が始まるまでに行ったこと
4.情報共有のためのページ作成
5.課題定義
6.要件定義
7.インセプションデッキ
8.設計
9.技術アーキテクチャ
10.ワイヤーフレーム
11.ロゴマークの作成
12.テーブル定義
13.デザイン
14.タスクばらし・担当決め
15.環境構築
16.発表用スライド作成
17.ガントチャートによる進捗管理
18.文字入力機能の実装
19.チュートリアル機能の実装
20.ヘルプ画面の実装
21.発表会当日までに実装できた機能
22.振り返り

2. メンバー構成

私のチームは3人で、プログラミング歴7ヶ月程の方が2名、プログラミング歴1年4ヶ月程の私。3人とも実務未経験で、チーム開発は前回のアプレンティスチーム開発に続いて2度目の体験でした。

3. チーム開発weekが始まるまでに行ったこと

チーム開発にだけに集中して取り組むことができるのは、1週間と決まっていました。
それまでの期間にミーティングを重ねて、チーム開発 week が始まったらすぐにコーティングをスタートできるように準備をしました。

以下はチーム開発weekが始まるまでに行ったことです。
4. 情報共有のためのページ作成
5. 課題定義
6. 要件定義
7. インセプションデッキ
8. 設計
9. 技術アーキテクチャ
10. ワイヤーフレーム
11. ロゴマークの作成
12. テーブル定義
13. デザイン
14. タスクばらし・担当決め
15. 環境構築
16. 発表用スライド作成

4. 情報共有のためのページ作成

Notion のチーム専用ページを用意し、チーム開発のルールや参考資料、チームミーティングの記録などができるように整理しました。
また、開発スケジュールを共有できるよう、Notion でガントチャートを準備しました。
ガントチャートの開発Weekでの活用法については、別途後述します。

Notion をご存知ない方がいましたら、ぜひ使ってみることをオススメします!
Notion を導入したことで、チームミーティングの記録や参考資料、これからご紹介する一連の開発過程などを簡単に整理・共有することができました。

ちなみに、Notion にはコードを記述できる機能もあり、まさにエンジニア向けのツールと言えます。私は普段からエンジニア関連の学習内容を Notion でまとめています。

5. 課題定義

「ワクワクするものを開発せよ」
このお題に対して、まず私は「ワクワクとはなんだろう?」と考えました。

ワクワクとは、将来の夢、未知のもの、理想の自分など未来に対する期待である
というのが、私が導き出した答えです。

さらにそこから関連するものを色々と考えた結果思いついたのが、ビジョンボードでした。

ビジョンボードとは、自分の目標や願望を視覚化するボードです。文字や画像、絵などを使って、目標や夢、望む未来のイメージを表現します。

参考サイト:なりたい自分を実現するビジョンボードの作り方

現在、ビジョンボードを作るには、手帳やコルクボードなどに手作業で写真などを切り貼りして作成するか、Canvaやフォトショップなどのツールで作成するのが主流です。
しかし、これらは

  • 手作業だと時間がかかりすぎること
  • 自由度が高すぎること
  • デジタルツールを使いこなすハードルの高さ

が課題だと考えました。

これらの課題を解決する、誰でも簡単にビジョンボードを作成することのできるツールを作成してはどうかとチームに提案し、私の案が採用されました!

6. 要件定義

課題に対して、どのような状態になれば解決したと言えるのかを考え、以下のような内容が挙がりました。

  • 操作が簡単である
  • 説明通りに進めば、迷うことなく完成することができる
  • 面倒な工程がなく手軽に作成できる

7. インセプションデッキ

アジャイル開発で用いられるインセプションデッキも取り入れました。

参考:【導入必須】スクラム開発におけるインセプションデッキの作成方法・具体例

各問いに対しての私たちの回答をまとめたので、詳しくは下記をご覧下さい。

詳しくはこちら

問1.私たちはなぜここにいるのか

夢や理想・目標を形にしたいが、既存のツールでビジョンボードを作成するハードルを感じている人を対象にし、簡単に作成できるビジョンボードのフォーマットを提供したいと思ったから。

問2.エレベーターピッチ

 夢を叶えたい人向けのプロダクトです。
このアプリでは、 夢や目標を視覚化することができ、手作業で作るものや自由度の高いアプリ とは違って、手軽に、テンプレートに基づいてビジョンボードの作成ができます。

問3.パッケージデザイン

ワクワクがコンセプトなので明るめの配色(彩度を抑えて柔らかい色にする、パステルとか)。
スッキリめでおしゃれな雰囲気にする。

問4.やらないことリスト

やること:

  • 目標の入力・表示
  • ローカルのデバイスに保存されている画像の登録・表示
  • Pinterest からの画像の登録・表示
  • テンプレートの作成
  • 出来上がったボードを画像として保存
  • 目標の項目を作成
  • ヘルプ機能を作成

やらないこと:

  • 会員登録・ログイン機能
  • テンプレートの編集
  • 目標の振り返り機能

状況を見て決める:

  • SNS(Pinterest, Instagram) に共有
  • 2個目のテンプレート作成

問5.ご近所さんを探せ

  • チームメンバー
  • 同期
  • メンター

問6.技術的な解決策を描く

言語:HTML, CSS, JavaScript, PHP, SQL
フレームワーク:Next.js, Laravel
ライブラリ:React
API:Pinterest,Instagram
ツール:Git/GitHub, WSL2, Docker, MySQL, Figma

問7.夜も眠れない問題

  • 期限内に終わらない
    → スケジュール管理、こまめな進捗管理をする。
  • 実装方法が分からない
    → タスクを分割する。他の方法で実現できないか考える。

問8.期限を見極める

  • 中間目標(水曜日まで)を考えて振り返る(最低限リリースできるものを作る。それから追加していく)
  • 月曜日(1/29)までに実装方法を調べておく
  • 発表会前々日(2/2)にはほぼ完成させる
  • 前日(2/3)の夜には動作確認が終わってるようにする
  • 当日(2/4)は発表の練習ぐらいにする

発表会は2/4(日)なので、遅くとも前日には形になっている必要があり、余裕を持って金曜までに完成を目指そうということになりました。

問9.トレードオフスライダー

1 2 3 4 5
機能を全部揃える(スコープ遵守)
予算内に収める(予算遵守)
期日を死守する(納期遵守)
バグを出さない(品質遵守)

何よりも期限を遵守することを最優先に置きました。
予算は設定されていません。

問10.何がどれだけ必要か

  • スキル: 今まで学習してきたところ
  • 開発期間(1/29 - 2/4)の進捗管理: 個人がしたタスクばらしを全員でフィードバックする

私たちがこれまで学習してきたHTML,CSS,JavaScript,React,Next.js,PHP,Laravel,SQLの知識の中で作る必要がありました。
期限は決まっているので、その期限内で完成させることが必須です。

以上のインセプションデッキを話し合うことで、開発のイメージを具体化し、スコープや優先順位の認識の共有を図りました。

8. 設計

  • 入力するだけで簡単に作成できるように、ビジョンボードのテンプレートを作成する
  • チュートリアル機能を実装する
  • 作成例を作る
  • ヘルプページを作成する

9. 技術アーキテクチャ

使用技術は以下の通りです。
dev2.drawio.png

10. ワイヤーフレーム

Figma を使用したことがあるのはチーム内で私だけだったため、ワイヤーフレームのたたき台を作成させていただき、ミーティングで画面共有しながら完成させました。
ワイヤーフレームはこちら

11. ロゴマークの作成

Canva を使用して、チームメンバー3人で話し合いながら、ロゴマークを作成しました。
ビジョンボードの枠と、そこにある様々なvision、そこからあふれる夢や希望、といったイメージを形にしました。
アプリ名は、夢や未来を紡ぐという意味を込めて「WEAVE」を採用しました。

weave_logo.png

12. テーブル定義

テンプレートを選択したらその後は HTML として情報が保存されるため、templates テーブルは他のテーブルと紐づかずに独立しています。
boards テーブルに画像とテキストが複数含まれるため、それぞれ images テーブル、texts テーブルと一対多の関係になっています。
今回は会員登録機能の実装はしませんが、拡張性を加味して users テーブルも設けています。
database.png

13. デザイン

Figmaを使用できるのがチーム内で私だけだったため、デザイン案のたたき台を作成させていただき、このデザインで問題ないかチームメンバーに確認をしました。
結果的に、特に変更や修正もなく、そのまま採用になりました。

作成する際に意識したことは、以下の通りです。

  • ロゴマークの色をメインカラーとし、色相環でトライアド(トライアル)の関係にある色をピックアップして、ヘッダー・フッターや背景、アクセントカラーとして取り入れた
  • 色は大きくわけて3色程度にし、シンプルにした
  • ビジョンボードの写真が映えるように、背景は真っ白にした

Create Page.png

デザインデータはこちら

14. タスクばらし・担当決め

  • フロントエンドとバックエンドで担当を分ける方法
  • 機能ごとに担当を分ける方法

があり、それぞれがバランスよく学びを深めるために機能毎に担当を分ける方法が推奨されていたため、その方法を採用しました。

大きな括りとして、

  • テキスト登録機能
  • 画像登録機能
  • ビジョンボードの画像化
  • 画像ダウンロード機能
  • SNS へのシェア機能
  • ヘルプ画面

があり、私はその中のテキスト登録機能とヘルプ画面、併せてヘッダー・フッターのコーティングを担当することになりました。

15. 環境構築

フロントエンドとバックエンドそれぞれ Docker コンテナで環境構築しました。

  • フロントエンド
    バックエンドの Laravel との通信を加味し、Next.js の下記ライブラリをベースにさせていただきました。
    GitHub - laravel/breeze-next
    上記ライブラリに、Typescript も導入しました。

  • バックエンド
    Sail で Docker コンテナを立ち上げ、Dockerfile, docker-compose.yml をカスタマイズしました。
    Xdebug, PHP Code Sniffer, PHP Stan, PHP MD, Plant UMLを導入しました。

各プロジェクトディレクトリを GitHub リポジトリにアップし、チームで共有して開発していきました。

16. 発表用スライド作成

発表用の資料は Google スライドで作成しました。
1ページの情報量が多くならないように工夫しました。
以下、資料の一部です。
スクリーンショット 2024-02-07 1.37.59.png
スクリーンショット 2024-02-07 1.38.32.png
スクリーンショット 2024-02-07 1.38.58.png

17. ガントチャートによる進捗管理

前述した Notion のガントチャートを活用し、チームメンバーの進捗管理を行いました。
スクリーンショット 2024-02-07 1.41.00.png

各メンバーが自分の担当機能についてさらにタスクばらしをして、各タスクのステータスを進行中、完了、未着手とすることで、上のガントチャートの達成率がパーセントで表示されるようになっています。
私がテキスト登録機能で挙げたタスクは以下の通りです。
スクリーンショット 2024-02-07 1.42.22.png

ガントチャートは、以下のテンプレートを利用しました。
Notionのテンプレート:Gantt Chart

18. 文字入力機能の実装

ポイントとなった点についていくつか挙げてみます。

useState を使用した状態管理

今回は入力した内容の自動保存が走る設計だったので、各テキストエリアの入力値を state 管理し、 onClick イベントを設置して入力される毎に state が更新されるようにしたいと思いました。
別コンポーネントの state を共有しているので、useContext を使用しています。

const [board, setBoard, textBoxes] = useContext(BoardState)

また、一文字入力される毎に API を叩くのではパフォーマンスが悪いということで、setTimeout を利用し、5秒間入力がなかったらデータが送信されるようにしようとしていました。

  // 再レンダリングされても消えないように useRef でタイマーを保持
  const timer = useRef(null)

  const storeHtml = () => {
    console.log(board)
    // timer にまだタイマーがセットされていたら(5秒未経過)、そのタイマーは削除する
    if (timer.current) {
      clearTimeout(timer.current)
    }

    const requestUpdate = async () => {
      try {
        console.log(textBoxes)

        const request = {
          edited_html: htmlRef.current.innerHTML,
          textBoxes: textBoxes,
        }
        console.log(request)

        // データベースの更新リクエストの送信
        const response = await axios.put(
          `api/vision_boards/${board.board_id}`,
          request,
        )
        console.log(response)

        // state の更新
        setBoard({
          ...board,
          html_text: response.data.edited_html
        })
      } catch (err) {
        console.log(err)
      }
    }

    // timer に新しいタイマーをセット
    // また5秒からスタートなので、データ保存が延期されることになる
    timer.current = setTimeout(() => {
      // データ保存の処理
      console.log('html, text を保存する処理')

      requestUpdate()
      // 5 秒後に↑の処理を実行
    }, 5000)
  }
    return (
    <section id="modal_target" className={styles.template} ref={htmlRef}>
      <div className={styles.board}>
        <div className={styles.row}>
          <TextBox
            storeHtml={storeHtml}
            thisArea={'lifeStyle'}
            textCategory={'life_style'}
          />

          ...

components/Textarea.js

'use client'
import { useCallback, useRef, useContext } from 'react'
import styles from '@/components/create/TextBox.module.css'

import { BoardState } from '@/context/BoardContext'

export default function TextBox({ storeHtml, thisArea, textCategory }) {
  const [, , textBoxes, setTextBoxes] = useContext(BoardState)
  // 再レンダリングされても消えないように useRef で texTimer を保持
  const texTimer = useRef(null)

  // useCallback で再レンダリングされる度に実行されないようにする
  const handleTextChange = useCallback(
    e => {
      // テキストを state に保存
      console.log(thisArea)
      setTextBoxes(prevTextBoxes => {
        console.log({
          ...prevTextBoxes,
          [thisArea]: e.target.value,
        })

        return {
          ...prevTextBoxes,
          [thisArea]: e.target.value,
        }
      })

      // タイマーと html 保存の処理
      storeHtml()
    },
    [thisArea, setTextBoxes, storeHtml],
  )

  return (
    <textarea
      className={`${styles['text_box']} ${styles[textCategory]}`}
      maxlength="104"
      value={textBoxes[thisArea]}
      onChange={handleTextChange}></textarea>
  )
}

ところが、onChange イベントで更新関数を実行しても、データを送信するときに別のコンポーネントで参照する useContext で共有している state の値が初期値の空文字ままで、何度更新しても空文字のデータが送信されてしまうという問題に直面しました。
入力値が state に保存できて、保存(更新)された値を別コンポーネントで取得でき、データベースに保存する。それを繰り返し実行できる。その状態までたどり着くのに非常に時間を要しました。
ポイントは、再レンダリングのタイミング、useCallback の使い方だったようです。
長くなるのでここでは詳細は割愛しますが、いずれ記事にしたいと思っています。

19. チュートリアル機能の実装

スクリーンショット 2024-02-08 4.43.44.png
チュートリアルは、編集画面の上にモーダルとして表示されるようにしました。
モーダルは react-modalというライブラリを使用し、body 要素にマウントする形で実装しました。

import ReactModal from 'react-modal'

ReactModal.setAppElement('body')

ReactModal コンポーネントでラップすることで、body へマウントすることができます。
なお、モーダルの外側や内側への CSS 適用のため、className属性の他、overlayClassName属性に CSS Modules のクラスを指定しました。
他にも様々な属性が用意されています。

  return (
    <div>
      <ReactModal
        contentLabel="Tutorial Modal"
        isOpen={modalIsOpen}
        className={`${modalStyles.Modal} ${modalStyles.step}`}
        overlayClassName={`${modalStyles.Overlay} ${
          modalStyles['step' + step]
        }`}
        onAfterOpen={afterOpenModal}
        shouldCloseOnOverlayClick={false}
        onRequestClose={closeModal}>
        
        {children}
        
      </ReactModal>
    </div>
  )

ブラウザの戻るボタンから一つ前のチュートリアル画面に戻れるように、チュートリアルのステップ毎にフォルダとpage.jsを作成し、props で渡した値によってステップ独自のコンポーネントが呼ばれるようにしました。

参考:モーダルの開閉状態を URL で管理する

ページの切り替えボタンには React Iconsを使用しました。

        <div className={modalStyles.changeStep}>
          {prev && (
            <div className={`${modalStyles.link} ${modalStyles.left}`}>
              <IconContext.Provider value={{ color: '#bff0f6', size: '80px' }}>
                <Link href={`/tutorial/step${prev}?board_id=${board_id}`}>
                  <FaArrowCircleLeft />
                </Link>
              </IconContext.Provider>
            </div>
          )}
          {next === 'last' ? (
            <div className={`${modalStyles.link} ${modalStyles.right}`}>
              <IconContext.Provider value={{ color: '#bff0f6', size: '80px' }}>
                <Link href={`/edit?board_id=${board_id}`}>
                  <CgCloseO />
                </Link>
              </IconContext.Provider>
            </div>
          ) : (
            <div className={`${modalStyles.link} ${modalStyles.right}`}>
              <IconContext.Provider value={{ color: '#bff0f6', size: '80px' }}>
                <Link href={`/tutorial/step${next}?board_id=${board_id}`}>
                  <FaArrowCircleRight />
                </Link>
              </IconContext.Provider>
            </div>
          )}
        </div> 

前のページ、次のページがあれば、ボタンを表示する記述をしています。
IconContextというのが、React icons にクラスを適用するためのプロバイダーで、その中にあるのがLinkコンポーネントと、React icons のコンポーネントです。

20. ヘルプ画面の実装

ヘルプ画面は、質問項目をクリックすると state が更新され、true/false によって、表示するコンポーネントを切り替えるという方法を取りました。

const [isOpen, setIsOpen] = useState({
    q1: true,
    q2: false,
    q3: false,
    q4: false,
  })

  const toggleOpen = questionNum => {
    setIsOpen(prev => {
      return {
        ...{
          q1: false,
          q2: false,
          q3: false,
          q4: false,
        },
        [questionNum]: true,
      }
    })
  }

質問のリストにonClickイベントリスナーを設置し、クリックされたら state 更新のための関数に 該当の質問番号を渡し、該当の質問番号の state が true、それ以外が false になるようにしています。

<ul>
  <li className={styles.list}>
    <button
      onClick={() => {
        toggleOpen('q1')
      }}>
      <h3 className={isOpen.q1 && styles.selected}>
        Q1 : Vision Board とはなんですか?
      </h3>
    </button>
  </li>
  <li className={styles.list}>
    <button
      onClick={() => {
        toggleOpen('q2')
      }}>
      <h3 className={isOpen.q2 && styles.selected}>
        Q2 : 各項目について詳しく知りたい
      </h3>
    </button>
  </li>
  <li className={styles.list}>
    <button
      onClick={() => {
        toggleOpen('q3')
      }}>
      <h3 className={isOpen.q3 && styles.selected}>
        Q3 : 目標の記入・保存方法について
      </h3>
    </button>
  </li>
  <li className={styles.list}>
    <button
      onClick={() => {
        toggleOpen('q4')
      }}>
      <h3 className={isOpen.q4 && styles.selected}>
        Q4 : 画像のアップロード方法を教えて!
      </h3>
    </button>
  </li>
</ul>

state の値が true、つまりクリックされた質問に該当するコンポーネントが表示されるようにしました。

<div className={styles.bottom}>
  {(isOpen.q1 && <Answer1 />) ||
    (isOpen.q2 && <Answer2 />) ||
    (isOpen.q3 && <Answer3 />) ||
    (isOpen.q4 && <Answer4 />)}
</div>

app/edit/help/Answer3.js

import styles from './Help.module.css'

export default function Answer3() {
  return (
    <>
      <div className={styles.answerArea}>
        <h3>Q3 : 目標の記入・保存方法について</h3>
        <br />
        <h3>A3 : 目標の記入・保存方法</h3>
        黄色のボックスが目標の記入欄です。
        <br />
        項目につき一つの入力エリアが用意されていますので、自由に入力してください。
        <br />
        枠からはみ出てしまったテキストは画像として保存する際に表示されないので注意が必要です。
        <br />
        入力したテキストは自動で保存されます。
        <br />
      </div>
    </>
  )
}

スクリーンショット 2024-02-08 4.20.54.png

21. 発表会当日までに実装できた機能

1週間という短い開発期間でチームとして実装できた機能は、以下の通りです。

  • 目標の入力・データベースへの登録・表示
  • 画像のアップロード・データベースへの登録・表示
  • テンプレートについて説明するチュートリアルの表示
  • テンプレート選択(トップページ)
  • 項目をクリックすると回答が表示されるヘルプページ
  • 作成したビジョンボードをHTMLとして保存する
  • サムネイルとして画像化したデータをデータベースに保存する
  • データベースに保存されたサムネイルをビジョンボード一覧ページに表示する

逆に実装できなかったのが、以下の通りです。

  • HTML データを画像化する
  • 保存されているサムネイルの画像をダウンロードする
  • 保存されているサムネイルの画像をSNSにシェアする

上2つについては、最低限実装したかった部分だったのですが、複雑な JSX のデータを画像化することの難しさや、download 属性を持たせた a 要素をクリックしてもダウンロードできないなどの壁にぶつかり、実現できませんでした。
ダウンロードすること自体は発表日当日に実現したのですが、それをページに組み込むのが発表までに間に合いませんでした。
3つ目については、上2つが実現できたら挑戦する予定だったので、手つかずとなりました。

GitHub リポジトリ
WEAVE - Client
WEAVE - Server

22. 振り返り

今回のチーム開発では、ここまでは実装したいというラインに、期限内に届かなかったことが悔やまれます。ですが、学べたことは非常に多く、特に React の基本思想であるコンポーネントの概念を、作りながら学ぶことができたと感じています。
また、自分の課題として state やレンダリング周りを改めて整理する必要性を認識できたのも大きいです。新しいサーバーコンポーネントと既存のクライアントコンポーネントの使い分けなどについても、まだまだ学ぶ必要があります。
そして、個人ではなくチームで開発するにあたってのチームメンバーとの連携や、進捗管理、疎結合なコンポーネントを作成することで互いのコードへの影響を最小限にすることについても学べたと思います。
チーム一丸となって1週間を走りきったこと、それが何よりも嬉しいです!

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?