こんにちは、バックエンドコースのmasayaです。
今回は、WILL(PlayGround早稲田大学)所属のメンバー同士でPGコース診断アプリを開発しましたので、その概要とチーム開発の流れ、およびその振り返りについてお伝えしていきたいと思います。
🚀メンバーと作成物について
メンバー
masaya、higashiji、Yusuke
作成物
使用技術
React、MaterialUI、Figma
デプロイ
Vercel
📕チーム開発を実施するに至った理由とその目的
理由
チーム開発を実施した理由はPlayGroundコミュニティの活性化を促す為。案件前にチーム開発を経験することで、案件までの敷居が下がり、参加しやすくなると考えたため。
目的
今回のチーム開発を先例に、PGメンバー同士でチーム開発がしやすい状況を作り上げていきたいと考えています。GitHubを使用したチーム開発の具体的内容を載せているので、参考にしてください。
📘チーム募集とチーム開発開始の時期
募集開始
12月にWILLでメンバー募集
チーム開発開始
1月は大学の期末試験があったため、2月から開始
📗ミーティングで話し合った内容について
メンバー同士で空いている時間を聞き、週1回30分のミーティングを開く方針に。ミーティングはZOOMで実施。
2月のミーティング
- 何を開発するか
- コンセプトは、「PGの新入生向けに何か役立つものは作れないか」
- コンセプトを考慮した結果、PGコース診断アプリに決定(こちらはすんなり決まった)
- いつ頃に完成を目指すか
- 4月を目処に完成を目指す方針へ。遅くても5月以内に完成。
- 診断アプリを作成するのに必要な技術について
- Reactが良いだろうと考え、Reactに決定。→ Reactの勉強を2月のメインにする方針へ
- デザインについてはFigmaで作成。MUIを使用したデザインを意識。こちらは3月から行う方針へ
- 診断アプリはどのような仕組みで作成されているのかを調査
- 問題ごとに点数が入る得点式と、回答によって次の問題が変わるフローチャート式が主らしい。フローチャート式は問題数を多く作る必要があり、大変そうだったので、得点式で作成する方針に決定。
- コース診断なので、各コースに合う問題の作成
- 問題数は多くない方が良いと考え、基礎コース5個 x 2の10問を作成する方針のもと、まずは各コース2問以上を各々が作成。
- 各自が作成した問題を見定め、どういった問題が好ましいかについて話し合い。
- 複数コースにまたがる質問もあったので、複数コースに当てはまる問題も考慮する方針に。
3月のミーティング
- 問題の内容と点数の入り方について
- 問題内容を絞り、10問に確定させた
- 点数の入り方に関して、「はい」「いいえ」「どちらでもない」の選択肢で決めることに決定。(点数の詳しい入り方は後の技術内容のところで紹介します)
- Figmaを使用してデザインを各自が考え、作成
- トップページ、問題ページ、結果ページがあればデザインはOKだと考え、この3ページを各自が作成する方針へ(期限は1週間)。
- 作成した結果、higashijiの作成したデザインを採用。
- 問題と点数の入れ方、デザインが作成できたので、いよいよコーディング作業へ
- GitHubでOrganizationsを作成し、そこにリポジトリを作成しチーム開発をすることに決定
- チーム開発の進め方に関して、issueを立ててPRを出す方針に決定
- 誰がどこを担当するのかについては、トップページ、問題ページ、結果ページのページ単位で取り決め。
- デプロイに関しては、firebaseを最初に考えていたが、ここは後にVercelに変更
3月後半から5月頭の完成まではコーディング作業で、ミーティングでは各自の進み具合について話し合った。
📙チーム開発の進め方
README.mdにチーム開発の方針を記述。
基本ルール
- git cloneを行う。
- コード整形ツールであるprettierを使用する。
- issueとPRの出し方について
- 自分の担当箇所について任意の粒度でissueを立てる
- issue番号を含むbranchを切って作業
git checkout -b issue番号/何をするか 例) git checkout -b 12/create-appearance-of-top
- 完成したらPRを出し、自分以外の誰かにレビューをお願いする
コンポーネントの書き方について
- 関数コンポーネントで書く
- スタイリングには基本的にMUIのsxを使う
- 担当箇所内でのコンポーネントの分割は自由
- 保守性やベストプラクティス等は気にしすぎず完成を優先させる
🎓開発技術
実際にアプリを開きながら確認していただくとどこの事を説明しているのか分かり易いと思います。
PGコース診断アプリ
前提として、topコンポーネント、questionsコンポーネント、resultコンポーネントの3つの大きな括りがあります
topコンポーネント、questionsコンポーネント、resultコンポーネントのコンポーネント切り替えの仕組み
- 下記のように、top、questions、resultの値をuseStateで管理し、topコンポーネントの「診断開始」を押すと、questionsの値が渡され、questionsコンポーネントが表示される。questionsコンポーネントで10問目の問題を答えると、resultの値が渡され、resultコンポーネントが表示される。resultコンポーネントの「もう一度」を押すと、画面がリロードされ、初期値のtopの値が入り、topコンポーネントが表示される。
- 実際なら、ルーティングを使用、すなわち、エンドポイントと特定のコンポーネントを結びつけるのが一般的だと思われるが、React学び始めだったため、使用しない方向で実施しています。
src/App.js
const [page, setPage] = useState("top");
(中略)
{/* pageの文字列によってどのコンポーネントを表示するかが決まる */}
{page === "top" ? (
<Top setPage={setPage} />
) : page === "questions" ? (
<Questions setPage={setPage} />
) : page === "result" ? (
<Result />
) : (
console.warn("pageの値が不正です")
)}
スコア計算の仕組み
- スコア計算は、「はい」を選んだら該当のコースに+1点。「いいえ」を選んだら該当のコースに-1点。「どちらでもない」を選んだらどのコースにも点数は入らないようになっています。点数の入るところはconsole.logで表示させているので、検証ツールのconsoleから確認いただけます。
- スコア計算はquestionsコンポーネント、resultコンポーネントでそれぞれ使用するため、useScoresというカスタムフックを作成。また、createContextを使用し、propsのバケツリレーをしないようにグローバルステートとする。また、関数の再レンダリングを防ぐためにuseCallbackを使用しています。
src/hooks/useScores.js
import { useState, createContext, useContext, useCallback } from "react";
const ScoresContext = createContext();
const IncrementContext = createContext();
const DecrementContext = createContext();
/**
* 各コースのスコアを管理するカスタムフック
*/
const useScores = () => {
const [frontend, setFrontend] = useState(0);
const [backend, setBackend] = useState(0);
const [design, setDesign] = useState(0);
const [mobile, setMobile] = useState(0);
const [data, setData] = useState(0);
const scores = {
frontend,
backend,
design,
mobile,
data,
};
//文字列を引数にとり、該当するコースのスコアを1増やす
const increment = useCallback((course) => {
switch (course) {
case "frontend":
setFrontend((value) => value + 1);
break;
case "backend":
setBackend((value) => value + 1);
break;
case "design":
setDesign((value) => value + 1);
break;
case "mobile":
setMobile((value) => value + 1);
break;
case "data":
setData((value) => value + 1);
break;
default:
console.warn("incrementの引数が不正です");
}
}, []);
//文字列を引数にとり、該当するコースのスコアを1減らす
const decrement = useCallback((course) => {
switch (course) {
case "frontend":
setFrontend((value) => value - 1);
break;
case "backend":
setBackend((value) => value - 1);
break;
case "design":
setDesign((value) => value - 1);
break;
case "mobile":
setMobile((value) => value - 1);
break;
case "data":
setData((value) => value - 1);
break;
default:
console.warn("decrementの引数が不正です");
}
}, []);
return { scores, increment, decrement };
};
export const ScoresProvider = ({ children }) => {
const { scores, increment, decrement } = useScores();
return (
<ScoresContext.Provider value={scores}>
<IncrementContext.Provider value={increment}>
<DecrementContext.Provider value={decrement}>
{children}
</DecrementContext.Provider>
</IncrementContext.Provider>
</ScoresContext.Provider>
);
};
export const useScoresData = () => {
return useContext(ScoresContext);
};
export const useIncrement = () => {
return useContext(IncrementContext);
};
export const useDecrement = () => {
return useContext(DecrementContext);
};
はい、どちらでもない、いいえのいずれかを押さないと、次へが押せない仕組み
- disabled属性によって管理している。disabledはbutton要素を無効にする属性でhtmlに備わっているもの。これは論理属性であり、disabledの値がtrueの限り、ボタンは押せない状態。だから、はい、どちらでもない、いいえのいずれかのボタンを押したらfalseになるように設定する。
- const disabled = !alignment || isFinished;より、alignmentに値が入り、isFinishedがfalseのとき、disabledはfalseとなる。
src/questions/QAndA.jsx
// alignmentは現在どのボタンが押されているかを管理するステート
const [alignment, setAlignment] = useState(undefined);
const disabled = !alignment || isFinished;
(中略)
<Button
variant="contained"
onClick={handleOnClick}
disabled={disabled}
sx={{ marginBottom: "20px", maxWidth: "400px", width: "100%" }}
>
次へ
</Button>
点数比較の仕組み
- カスタムフックであるuseScoresDataにquestionsコンポーネントで得た点数が渡されている状態。そして、coursesという配列の中に、各コースごとのオブジェクトがあり、さらにその中のvalueプロパティに各コースの点数が格納されている。
- sort()メソッドにより、元の配列が並び替えられるのを防ぐため、slice()メソッドにより、元の配列と同じ配列をコピーしている。そうすることで、安心してsort()メソッドを使用できる。
- sort()メソッドにより、sliceメソッドによってコピーされたcourses配列の並び替えを行う。引数にa, bを指定。なぜ引数が2つなのかというと、数値比較を行うためである。比較は2つ値がないとできないので。この引数には、配列の要素が格納されている。したがって、a.valueとすれば、各コースの点数を取り出し、点数比較ができる状態となる。
- return -1;を返す場合、1つ目の要素を2つ目の要素より小さインデックスにする。return 1;を返す場合、2つ目の要素を1つ目の要素より小さいインデックスにするので、a.value > b.valueの時は、return -1を返し、a.value < b.valueの時は、return 1を返すことで、点数が大きい順に並び替えられる。点数が同点の場合は、Math.random()関数を実施し、0.5より大きい値が出れば、1を、0.5以下なら-1を返してランダムに点数を付し、並び替える。
src/result/Result.jsx
const scores = useScoresData();
const courses = [
{
title: "フロントエンド",
value: scores.frontend,
detail: `最前線で働きたい...`,
img: img_front,
},
.
(中略)
.
{
title: "デザイン",
value: scores.design,
detail: `創作表現が好きで...`,
img: img_design,
},
];
/* 各コースの得点比較 */
const sortedCourses = courses.slice().sort((a, b) => {
if (a.value > b.value) {
return -1;
} else if (a.value < b.value) {
return 1;
} else {
return Math.random() > 0.5 ? 1 : -1;
}
});
📚振り返り
発案段階
良かった点
- 規模感がちょうどよかった。
- 4・5月に完成を目指していたので、それが達成できた。
- とっつきやすい題材で、考慮すべき事柄が少なかった。
- PlayGroundのメンバーに割とウケたのが嬉しかった。
反省点
- バックエンドのメンバーがいたから、フロントバックが連携できるアプリの方が役割分担・チーム開発感の観点からはよかったかも。
- 問題作成に時間をかけすぎてしまった。ここをもっとReactの知見を深められる場を設けられるようにしていきたかった。
設計・デザイン段階
良かった点
- それぞれのメンバーがすべてのプロセスに関与できた。
反省点
- もう少し大規模になったらあらかじめ役割分担を決めて、タスクとして割り振り、定例で全体確認というフローだとスムーズかも。
- デザイン案が決まった後には清書をするべきだった。
- 細かい余白等がかなり適当だったので、実装時に負担になってしまった
- レスポンシブ対応がかなり行き当たりばったりになってしまったので、清書時点でちゃんと決めておくべき
実装段階
良かった点
- UIライブラリを使うことで最低限の見た目の統一感がでた。
- 得点の管理の方法など、お互いで話し合って決めることができた。
反省点
- レビューが微妙な感じになってしまった。後半はセルフマージに。
- Reactができるメンバーにかなりの部分を頼ってしまい、コード量のバランスが取れていなかった。
📖最後に
- チーム開発は私の思いつきで行ったものでしたが、メンバー同士できちんと役割分担して、開発が進められたのはとても良かったと思います。改めて、メンバーとして開発をしてくれたhigashijiとYusukeに感謝します。
- この記事が、これからのチーム開発に役立てればと思います。文量は多くなってしまいましたが、かなり具体的に書きましたので、反省点は多いものの、それも含めて次に活かせるものになるのではないかと思っています。