はじめまして。株式会社PRUMでエンジニアをしている人見です。
日々、プログラミング学習や実務の中で、つまずきやすいポイントや、
仕事で起きやすい“ズレ”について整理して発信しています。
誰かの助けになれば幸いです。
9割のエンジニア未経験者がつまずく『最初の壁』 それでもアプリを作りたい -30分でアプリを作ってみよう -
はじめに
前回は、「そもそもアプリって何だろう?」
という話をしました。今回は実際にアプリを作ってみます。
とはいっても、
- Javaのインストール
- Pythonのインストール
- Docker
- AWS
みたいな難しい準備はありません。
Googleアカウントさえあれば大丈夫です。
まずは、「自分でもアプリを作れた!」
という体験をしてみましょう。
今回作るもの
今回作るのはこちらです。
- 参加する
- 不参加にする
を登録できる簡単なアプリです。
登録された内容はスプレッドシートに保存されます。
STEP1 スプレッドシートを作成する
まずはGoogleを開きます。
【画像① Googleホーム画面】右上のアプリ一覧を選択します。

STEP2 スプレッドシートに名前を付ける
STEP3 Apps Scriptを開く
上部メニューから
拡張機能 → Apps Script
を選択します。
すると以下のような画面が表示されます。
STEP4 プログラムを書く
【画像⑧】最初から入っているコードを削除して、以下のコードを貼り付けます。

Code.gs ← 貼り付けるコードはコチラ
ここをクリックすると展開されます
const SHEET_NAME_RESPONSES = '回答一覧';
const SHEET_NAME_MEMBERS = 'メンバー一覧';
const EVENT_NAME = '本日の朝会';
function doGet() {
setupSheets_();
return HtmlService.createHtmlOutputFromFile('index')
.setTitle('参加・不参加確認アプリ');
}
function submitAnswer(data) {
setupSheets_();
const name = String(data.name || '').trim();
const status = String(data.status || '').trim();
const memo = String(data.memo || '').trim();
if (!name) {
throw new Error('名前を入力してください。');
}
if (status !== '参加' && status !== '不参加') {
throw new Error('参加または不参加を選択してください。');
}
registerMemberIfNeeded_(name);
upsertResponse_(name, status, memo);
return getSummary();
}
function getSummary() {
setupSheets_();
const members = getMembers_();
const responses = getLatestResponses_();
const joined = [];
const absent = [];
const unanswered = [];
members.forEach(name => {
const response = responses[name];
if (!response) {
unanswered.push(name);
return;
}
if (response.status === '参加') {
joined.push({
name: name,
memo: response.memo,
updatedAt: response.updatedAt
});
return;
}
if (response.status === '不参加') {
absent.push({
name: name,
memo: response.memo,
updatedAt: response.updatedAt
});
return;
}
});
return {
eventName: EVENT_NAME,
total: members.length,
answered: joined.length + absent.length,
joinedCount: joined.length,
absentCount: absent.length,
unansweredCount: unanswered.length,
joined: joined,
absent: absent,
unanswered: unanswered,
generatedAt: Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm')
};
}
function sendSlackSummary() {
const webhookUrl = PropertiesService.getScriptProperties().getProperty('SLACK_WEBHOOK_URL');
if (!webhookUrl) {
throw new Error('スクリプト プロパティに SLACK_WEBHOOK_URL が設定されていません。');
}
const summary = getSummary();
const message = buildSlackMessage_(summary);
const payload = {
text: message
};
const options = {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify(payload),
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(webhookUrl, options);
const statusCode = response.getResponseCode();
if (statusCode < 200 || statusCode >= 300) {
throw new Error('Slack通知に失敗しました。status=' + statusCode + ' body=' + response.getContentText());
}
}
function createDailySlackTrigger() {
deleteDailySlackTriggers_();
ScriptApp.newTrigger('sendSlackSummary')
.timeBased()
.everyDays(1)
.atHour(8)
.create();
}
function deleteDailySlackTriggers_() {
const triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getHandlerFunction() === 'sendSlackSummary') {
ScriptApp.deleteTrigger(trigger);
}
});
}
function buildSlackMessage_(summary) {
const joinedNames = summary.joined.length > 0
? summary.joined.map(item => '・' + item.name).join('\n')
: 'なし';
const absentNames = summary.absent.length > 0
? summary.absent.map(item => '・' + item.name).join('\n')
: 'なし';
const unansweredNames = summary.unanswered.length > 0
? summary.unanswered.map(name => '・' + name).join('\n')
: 'なし';
return [
'【参加確認】' + summary.eventName,
'',
'現在 ' + summary.total + '人中 ' + summary.answered + '人が回答済みです。',
'',
'参加:' + summary.joinedCount + '人',
'不参加:' + summary.absentCount + '人',
'未回答:' + summary.unansweredCount + '人',
'',
'■ 参加',
joinedNames,
'',
'■ 不参加',
absentNames,
'',
'■ 未回答',
unansweredNames,
'',
'未回答の方は、参加確認をお願いします。'
].join('\n');
}
function setupSheets_() {
const spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
let responsesSheet = spreadsheet.getSheetByName(SHEET_NAME_RESPONSES);
if (!responsesSheet) {
responsesSheet = spreadsheet.insertSheet(SHEET_NAME_RESPONSES);
responsesSheet.appendRow(['更新日時', '名前', '回答', 'メモ']);
}
let membersSheet = spreadsheet.getSheetByName(SHEET_NAME_MEMBERS);
if (!membersSheet) {
membersSheet = spreadsheet.insertSheet(SHEET_NAME_MEMBERS);
membersSheet.appendRow(['名前']);
membersSheet.appendRow(['佐藤']);
membersSheet.appendRow(['鈴木']);
membersSheet.appendRow(['田中']);
membersSheet.appendRow(['山田']);
membersSheet.appendRow(['高橋']);
}
}
function getMembers_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_MEMBERS);
const values = sheet.getDataRange().getValues();
return values
.slice(1)
.map(row => String(row[0] || '').trim())
.filter(name => name);
}
function getLatestResponses_() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_RESPONSES);
const values = sheet.getDataRange().getValues();
const responses = {};
values.slice(1).forEach(row => {
const updatedAt = row[0];
const name = String(row[1] || '').trim();
const status = String(row[2] || '').trim();
const memo = String(row[3] || '').trim();
if (!name) {
return;
}
responses[name] = {
status: status,
memo: memo,
updatedAt: Utilities.formatDate(new Date(updatedAt), 'Asia/Tokyo', 'yyyy/MM/dd HH:mm')
};
});
return responses;
}
function registerMemberIfNeeded_(name) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_MEMBERS);
const members = getMembers_();
if (!members.includes(name)) {
sheet.appendRow([name]);
}
}
function upsertResponse_(name, status, memo) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(SHEET_NAME_RESPONSES);
const values = sheet.getDataRange().getValues();
for (let i = 1; i < values.length; i++) {
const rowName = String(values[i][1] || '').trim();
if (rowName === name) {
const rowNumber = i + 1;
sheet.getRange(rowNumber, 1, 1, 4).setValues([[
new Date(),
name,
status,
memo
]]);
return;
}
}
sheet.appendRow([
new Date(),
name,
status,
memo
]);
}
STEP5 HTMLファイルを作る
【画像9】 左上の「+」ボタンから、「HTML」を選択し、HTMLファイルを作成します。

【画像⑪】最初から入っているコードを削除して、
以下のコードを貼り付けます。

index.html ← 貼り付けるコードはコチラ
ここをクリックすると展開されます
```html参加・不参加確認アプリ
ボタンを押すだけで参加確認ができるGASデモアプリです。
回答はスプレッドシートに保存され、集計結果をSlackに自動通知できます。
<section class="grid">
<div class="card">
<h2>回答する</h2>
<label for="name">名前</label>
<input id="name" type="text" placeholder="例:佐藤">
<label for="memo" style="margin-top: 16px;">メモ</label>
<textarea id="memo" placeholder="任意:少し遅れます、午前だけ参加など"></textarea>
<div class="button-row">
<button id="joinButton" class="join" onclick="submitAnswer('参加')">参加</button>
<button id="absentButton" class="absent" onclick="submitAnswer('不参加')">不参加</button>
</div>
<div id="status" class="status"></div>
</div>
<div class="card">
<h2>現在の集計</h2>
<div id="summaryArea">
読み込み中です...
</div>
<button class="reload" onclick="loadSummary()">最新に更新</button>
</div>
<div class="card full">
<h2>回答一覧</h2>
<div id="listArea">
読み込み中です...
</div>
</div>
</section>
STEP6 Webアプリとして公開する
【画像⑫】 右上の「デプロイ」 → 「新しいデプロイ」 を選択します。

STEP8 URLを開く
【完成!】
これでアプリが完成しました。
今作ったものも立派なWebアプリです。
なぜアプリになったの?
不思議ですよね。私たちは
- サーバーを契約していない
- Javaもインストールしていない
- AWSも使っていない
それなのに、インターネットからアクセスできるアプリができました。
実はGoogleが
- サーバー
- 実行環境
- 公開機能
を用意してくれているからです。
次回予告
今回は 「画面を表示する」ところまでできました。
やっぱり実際に動かした瞬間が一番楽しいですよね!!
次回は、これのどこがアプリなのかについて触れるとともに、
アプリ作りの楽しい部分に触れていきましょう。
PRUMのエンジニアの95%以上は未経験からの採用です。
もし弊社にご興味あれば覗いてみてくださいね。














