こまどブログ

技術のこととか仕事のこととか。

Node.jsからConnpassのAPIを叩こうとしてつまずいたこと

はじめに

この記事をご覧になっている方の多くは、技術系の勉強会に参加された経験があるのではないかと思います。僕はよく参加しています。

他の業界の友人と話をして、最も驚かれるのが「業後に勉強会に出ている」ということです。毎日のようにどこかで勉強会が開かれ、様々な企業の人が職業に関わる情報をやりとりしているのはこの業界の特色でしょう。

さて、そんな勉強会の情報は、いくつかのイベント情報共有サービスを通じて周知されているものがほとんどだと思います。ConnpassやDoorkeeper、ATNDなどが有名どころでしょうか。それらのサイトを訪問すれば、日々新しい勉強会の開催情報が公開されているのを見ることができます。

しかし、サイトを訪問したり、公式のメール配信に登録する以外にも、勉強会の情報を得る方法があります。それはAPIを利用することです。APIを利用することで、特定の条件を満たすイベントの情報を自動で取得でき、様々なツール(SlackやTwitter)に送信することもできます。

そこで今回は、

  • ConnpassのAPIを利用する手順
  • 利用するに当たってつまずいたこと

についてご紹介します。

Connpass APIは登録なしですぐ使える

今回利用するのは、ConnpassのAPIです。無料で公開されています。

WebサービスAPIの中には、登録が必要なものもありますが、Connpassの場合は利用登録は必要ありません。指定されたURL( https://connpass.com/api/v1/event/)に対してHTTPリクエスト(GET)を送信するだけで、イベント情報を取得することができます。

Connpass APIでイベントの情報を取得するサンプルコード

実際に叩いてみましょう。以下のような実装で取得することができます。まだ途中ですが、Lambdaで定期実行するようにしたいので、Lambda向きの書き方になっています。

const https = require('https');
const queryString = require('querystring');

const getResponse = async (opts, queries)=>{
  opts.path +='?' + queryString.stringify(queries);  // クエリ文字列を生成してパスの末尾に付加
  console.log('path:', opts.path);
  return new Promise((resolve, reject)=>{  // httpsモジュール、Promiseについては前回記事(下記)を参照
    https.get(opts, (response)=>{
      console.log('statusCode:', response.statusCode);
      console.log('statusMessage:', response.statusMessage);
      response.setEncoding('utf8'); // Buffer型ではなくUTF-8の文字列で取得する
      let chunkCount = 0;
      response.on('data', (chunk)=>{
        chunkCount ++;
        body += chunk;
      });
      response.on('end', ()=>{
        console.log('チャンクの個数',chunkCount);
        let bodyObj = JSON.parse(body); // レスポンスはJSON形式で返ってくるのでパースする
        console.log('検索結果件数:', bodyObj.results_available);
        console.log('うち取得件数:', bodyObj.results_returned);
        console.log('1つめのイベント:', bodyObj.events[0].title);
        resolve(bodyObj);
      });
    }).on('error', (err)=>{
      console.log('error:', err);
      reject(err);
    });
  });
};

exports.handler = async ()=>{
  let opts = {
      hostname: 'connpass.com',
      path: '/api/v1/event/',
      headers: {
        'User-Agent': 'Node/8.10'  // これがないと拒否される
      }
  };
  const queries = {
    'keyword_or': [
      'アジャイル',
      'javascript'
    ]
  };
  let result = await getResponse(opts, queries);
  return result;
};

参考(httpsモジュールによるリクエストの送信)

ky-yk-d.hatenablog.com

つまずきポイント①403エラーとUser-Agentヘッダー

今回、ConnpassのAPIを利用しようとして、最初につまずいたのは、403 Forbiddenになってしまうという事象でした。Web APIを利用した経験がほぼゼロであるため、解決方法が分からずしばらく苦しみましたが、以下の記事に行き着きました。

上の記事は、GitHubAPIへのリクエストが403 Forbiddenとなっていたのを、User-Agentヘッダーを付与することで解決したというものです。User-Agentについては、以下の記事が端的でわかりやすかったです。リクエストを送信したのが何者なのかをサーバ側に教えてあげるためのものというわけですね。

www.atmarkit.co.jp

上記のQiita記事を読んだときに、「たぶんこれだ」と直感し、https.request()の第一引数のoptionsに、User-Agentヘッダーを付加して送信するように指定してあげました。すると思った通り、200 OKが返ってくるようになったではありませんか。Web APIを叩くときは要注意ですね。

なお、Node.jsからの実行なので、試しにNode/8.10と記載してみましたが、書き方をご存知の方がいらっしゃればご教示いただけると幸いです。

修正前

let opts = {
    hostname: 'connpass.com',
    path: '/api/v1/event/'
};

修正後

let opts = {
    hostname: 'connpass.com',
    path: '/api/v1/event/',
    //  ---以下を付加---
    headers: {
        'User-Agent': 'Node/8.10'
    }
    // ---追加部分終わり---
};

つまずきポイント②日本語のクエリ文字列のエンコード

つまずいた第二のポイントは、日本語のクエリ文字列のエンコードです。

ConnpassのAPIは、検索条件をクエリ文字列としてリクエストに付加して利用するのですが、そのなかの検索ワードは文字列で送信することになります。英数字であれば、何も考えずに文字列として付加して送信すればよいのですが、日本語文字列を何も考えずに送信すると失敗します200 OKにはなりますが、検索文字列として機能しないので有意味なレスポンス本文が返ってきません。

1. 何も考えずに日本語文字列を送る例

最初、このようなやり方をしていて首を傾げていました。日本語文字列がそのままパスに含められて送信されています。①と違って、HTTPリクエストとしてエラーになるわけではありませんが、レスポンス本文が取得できていないため、本文から検索結果の情報を取得するところでエラーとなります。

HTTPによる通信に慣れている人であれば絶対につまずかない箇所なのだと思いますが、生成したクエリ文字列を含むパスをChromeのURL入力欄に放り込むとちゃんと検索結果が(エンコードされていない状態で)得られるので、自分が間違ったことをしていることに気づきませんでした。

ソースコード(一部)

opts.path +='?' + 'keyword=アジャイル';

実行結果

2018-07-22T13:30:53.242Z 74a3205b-8db3-11e8-80e4-430677743f08    path: /api/v1/event/?keyword=アジャイル
2018-07-22T13:30:53.547Z    74a3205b-8db3-11e8-80e4-430677743f08    statusCode: 200
2018-07-22T13:30:53.547Z    74a3205b-8db3-11e8-80e4-430677743f08    statusMessage: OK
2018-07-22T13:30:53.550Z    74a3205b-8db3-11e8-80e4-430677743f08    チャンクの個数 1
2018-07-22T13:30:53.586Z    74a3205b-8db3-11e8-80e4-430677743f08    検索結果件数: 0
2018-07-22T13:30:53.586Z    74a3205b-8db3-11e8-80e4-430677743f08    うち取得件数: 0
2018-07-22T13:30:53.587Z    74a3205b-8db3-11e8-80e4-430677743f08    TypeError: Cannot read property 'title' of undefined
    at IncomingMessage.response.on (/var/task/index.js:24:52)
    at emitNone (events.js:111:20)
    at IncomingMessage.emit (events.js:208:7)
    at endReadableNT (_stream_readable.js:1064:12)
    at _combinedTickCallback (internal/process/next_tick.js:138:11)
    at process._tickDomainCallback (internal/process/next_tick.js:218:9)

2. 自分でエンコードして送る例

日本語文字列を含める場合は、エンコードが必要になるということでした。そのための関数はちゃんと用意されていて、下記のようなやり方で適切な形に変換することができました。実行結果を見ると、きちんと日本語文字列がエンコーディングされて送信されていることがわかります。結果もちゃんと返ってきています。

ソースコード(一部)

opts.path +='?' + 'keyword='+ encodeURIComponent('アジャイル');

実行結果

2018-07-22T13:31:57.688Z 9b0f3f00-8db3-11e8-ab20-c3947964f9d8    path: /api/v1/event/?keyword=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB
2018-07-22T13:31:59.441Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    statusCode: 200
2018-07-22T13:31:59.446Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    statusMessage: OK
2018-07-22T13:31:59.468Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    チャンクの個数 8
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    検索結果件数: 775
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    うち取得件数: 10
2018-07-22T13:31:59.487Z    9b0f3f00-8db3-11e8-ab20-c3947964f9d8    1つめのイベント: 第2回enPiT-Proスマートエスイーセミナー: アジャイル品質保証と組織変革

3. querystringモジュールを利用する例

理屈の上では、2のようなやり方で足りますが、クエリ文字列の数が増えてくるとめんどくさそうです。エンコーディングについても、あまり意識したくはありません。これらの悩みを解決してくれるのが、Node.jsのモジュールであるquerystringモジュールです。

今回は、querystring.stringify()メソッドを利用しています。このメソッドでは、

  • オブジェクトからクエリ文字列への変換
  • パーセントエンコーディング(デフォルト。第四引数として関数を渡すことで変更することも可能)

を行ってくれます。また、下記の例では一つだけですが、keyword': ['aaa', 'bbb']と配列で指定してやると自動的に展開していい感じに処理してくれます。優れものですね。

ソースコード(一部)

const queryString = require('querystring');
let queries = {
    keyword: 'アジャイル'
};
opts.path +='?' + queryString.stringify(queries);

実行結果

2018-07-22T13:35:03.055Z 097210d9-8db4-11e8-a137-69efa6eedcfd    path: /api/v1/event/?keyword=%E3%82%A2%E3%82%B8%E3%83%A3%E3%82%A4%E3%83%AB
2018-07-22T13:35:03.468Z    097210d9-8db4-11e8-a137-69efa6eedcfd    statusCode: 200
2018-07-22T13:35:03.486Z    097210d9-8db4-11e8-a137-69efa6eedcfd    statusMessage: OK
2018-07-22T13:35:03.508Z    097210d9-8db4-11e8-a137-69efa6eedcfd    チャンクの個数 9
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    検索結果件数: 775
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    うち取得件数: 10
2018-07-22T13:35:03.527Z    097210d9-8db4-11e8-a137-69efa6eedcfd    1つめのイベント: 第2回enPiT-Proスマートエスイーセミナー: アジャイル品質保証と組織変革

おわりに

以上、ConnpassのAPIの叩き方のサンプルとつまずきポイントをご紹介しました。AWSAPI GatewayとLambdaを用いてAPIを作り、それをコールするというのは何度かやったことがありましたが、他人が作成したAPIを叩くというのは初めてだったので、思いの外つまずき、そして勉強することがありました。

現在、ConnpassのAPIを使ったボットを作成しようとしています。時間の使い方が下手でなかなか作業が進められておらず、まだ動くものになっていませんが、ソースはGitHubで公開していますので、ご興味ある方は眺めてみてください。

github.com

Node.jsのhttpsモジュールを用いた通信処理をPromiseで書き直して解読してみた

JS初心者がよくわからないまま書いたLambda

以前、このような記事を書きました。LINEのMessaging APIを用いた簡単なbotの実装の紹介記事です。実はこちらのブログで最も多くのブクマ(16ブクマ)を集めている記事となっています(2018.07.15現在)。

ky-yk-d.hatenablog.com

表題の通り、この記事を書いたのはJavaScriptを触り始めたころです。全体的に、見よう見まねでコーディングしていたのですが、中でも理解できていなかったのはhttpリクエストを送っている箇所です。この部分については完全にコピペ、意味も「あーなんか本文くっつけて送ってんだなー」くらいの理解でした。

記事の執筆から2ヶ月弱が経ち、この箇所について多少なりともわかってきたので、かつての自分と同レベルの方(来月くらいには自分もまたそこに戻っているかもしれない・・・)のためにメモを残しておくことにしました。httpリクエストを送信する部分を書き換えているので、そこを見ていきます。

書き換え(httpリクエスト送信部)

Before

書き換え前のコード

let https = require('https');
 
exports.handler = (event, context, callback) => {
// ---中略---
    var req = https.request(opts, function(res) {
        res.on('replyData', function(res) {
            console.log(res.toString());
        }).on('error', function(e) {
            console.log('ERROR: ' + e.stack);
        });
    });
    callback(null, replyData);
    req.write(replyData);
    req.end();
}

問題点/当時の理解レベル

  • requireは「httpsを使うためのおまじない」だった(Node.jsのhttpsを利用している、そしてそれがモジュールだという意識はなかった)
  • exportsを「なんか最初に実行されるやつでしょこれ」という認識だった(モジュールがわかっていないので外部公開も当然わかっていなかった)
  • on()でイベント登録をしている箇所で、bodyだったところをreplyDataに書き換えてしまっていた(イベントというものを理解していなかった)
  • 処理の途中でcallbackしていた(最後でなきゃいけないという決まりはないが、通信の結果を返すようにすべきだと今は思う)
  • callback()の第一引数がなぜnullなのかわかっていなかった
  • req.write()は「本文書き込んでいるんしょ」という認識(間違っていないけどWritable Streamということは意識していない)

After

書き換え後のコード

exports.handler = (event, context, callback) => {
// ---中略---
    let promise = sendRequest(opts, replyData).then((res)=>{
        console.log('---DONE---');
        console.log('typeof:', typeof(res));
        callback(null, res);
    },(err)=>{
        console.log('---ERROR---');
        callback(err, 'errorMsg' + err.stack);
    });
    console.log('typeof promise:', typeof(promise));
    console.log('promise:', promise);
}

// リクエスト送信部を切り出した関数
async function sendRequest(opts,replyData){
    return new Promise(((resolve,reject)=>{
        console.log('Promiseの引数の関数開始');
        let req = https.request(opts, (response) => {
            console.log('---response---');
            response.setEncoding('utf8');
            let body = '';
            response.on('data', (chunk)=>{
                console.log('chunk:', chunk);
                body += chunk;
            });
            response.on('end', ()=>{
                console.log('end:', body);
                resolve(body);
            });
        }).on('error', (err)=>{
            console.log('error:', err.stack);
            reject(err);
        });
        req.write(replyData);
        req.end();
        console.log('Promiseの引数の関数終了')
    }));
};

修正箇所

  • リクエスト送信部をハンドラ関数から切り出した。
  • httpsモジュールを用いた通信をPromiseでラップした.
  • 意識的にイベント登録をした。
  • 非同期処理が終わった後にcallbackが呼び出されるようにした。

処理の流れと解読

動作を確認するために余計なコードがたくさん含まれているのですが、このLambda関数の挙動は下記のような流れになっています(成功した場合)。順に解読していきます。ログも載せておきます。

テスト実行ログ

メッセージの場合
データ作成
---START---
Promiseの引数の関数開始
Promiseの引数の関数終了
typeof promise: object
promise: Promise { <pending> }
---response---
chunk: {"message":"Invalid reply token"}
end: {"message":"Invalid reply token"}
---DONE---
typeof: string

1. sendRequest()が呼び出され、引数の処理が行われる

ハンドラ関数がsendRequest()を呼び出すと、当然ながらnew Promise()が実行されます。引数に渡した関数の処理は、この時点で実行されるようですね。https.request()ClientRequestオブジェクトを返します(ここで接続=非同期処理が開始されていることになります)。

そして、ClientRequestオブジェクトに対して、on('error', ...)とすることでerrorイベントへのリスナーを登録しています。ClientRequestは、Writable Streamインターフェースを実装した出力ストリームの一つで、EventEmitterでもあります。EventEmitterは、Node.jsの特徴の一つであるイベントループの重要な構成要素と言ってよいと思います。

ちなみに、http(s).request()に第二引数(オプション)としてコールバック関数を渡すことで、responseイベントへのリスナーも登録していることになります。意味的にはonce('response', (...)=>{...})としても同じになるはずです。

The optional callback parameter will be added as a one-time listener for the 'response' event.

(参考)HTTP | Node.js v10.6.0 Documentation

そして、req.write()で本文を送信し、req.end()で出力ストリームを閉じています。いずれも、Writable Streamインタフェースで宣言されているメソッドです。ここまで終わると、Promiseオブジェクトが生成され、呼び出し元に返却されます。

2. 呼び出し元の処理が実行される

Promiseが返ってきたら、呼び出し元の処理が走ります。返却されてきたPromiseに対して、then()メソッドで成功ハンドラと失敗ハンドラを付加しています。Promiseを返した非同期処理の側でresolve()が実行された場合は第一引数の関数が、reject()が実行された場合は第二引数の関数が実行されることになります。

ハンドラの登録まで実行してから、promise変数に代入をし、型と内容を表示しています。この時点では、まだPromiseの中身の処理は行われておらず、Promiseは未解決(pending)となります。通常なら、awaitを付けてasync関数を呼び出し、解決後の値を変数に代入するのでしょうが、今回は動きの確認のためPromiseをそのまま代入しています。

3. 非同期処理の結果を受け取る

ここまでの間に、https.request()で開始した通信が行われています。JavaScriptはシングルスレッドなので、同時に複数の処理を実行することはできないのですが、時間のかかる処理を外部に依頼してしまって、終わるまでは別の処理を実行しておくことはできます。これが非同期処理ということですね。細かい理屈は現在調査中なので、後日まとめられればと思っています。

しばらく経つと、リクエストに対するレスポンスが返ってきます=responseイベントが発生します。このresponseイベントに対するリスナーが、http(s).request()の第二引数で登録したコールバック関数です。コールバック関数の引数(コールバックを渡された側が、呼び出し時に渡して返してくれるもの)は、IncomingMessageオブジェクトで、これはReadable Streamインタフェースを実装した入力ストリームです。送信するリクエストがWritable Streamだったのに対し、こちらはReadableとなるわけですね。

こちらもClientRequestと同様、EventEmitterなので、イベントリスナーを登録することができます。ここでは、dataイベントとendイベントのリスナーを登録しています。なお、これらのイベントは、IncomingMessage固有のものではなく、Readable Streamのものです。また、setEncodeing()エンコーディングを設定していますが、これもReadable Streamのものです。

二つのイベントの意味は、レスポンス本文が送られてきたら(dataイベントが発生したら)、順次body変数に格納し、全て取得し終えたら(endイベントが発生したら)resolve(body)を実行しろ、ということです。resolve()が実行された時点で、呼び出し元でpendingとなっていたPromiseが解決されます。

4. then()で登録しておいた成功ハンドラが実行される

Promiseが解決されると、呼び出し元であらかじめthen()で登録しておいた処理が実行され、解決した値(resolve()の引数)であるbodyの値が、resとして渡されてきます。ログにはresがstring型であると出力されますし、callbackした結果としても下記の通り、レスポンスの本文が表示されます。

f:id:ky_yk_d:20180716010212p:plain

ここまで実行されて、Lambda関数の処理が全て終了したことになります。めでたしめでたし。

今後の課題

書き換えて処理を追ったらずいぶん長文になってしまいました。簡潔にまとめるつもりだったのですが、書きながら裏取りのつもりで調べていたらその過程で学ぶことが多くありすぎ、まとめることを放棄しました(汗)

今回の書き換えと本記事の執筆のなかで、Promiseやasync/await、Node.jsのI/Oの仕組みなどに期せずして接しました。せっかく学ぶきっかけが得られたので、これからもう少し深く勉強して、後日自分なりにまとめてみようと思います!

Vue Routerのナビゲーションガードによるアクセス制限を試した&コードを読み解いた

前回の記事に引き続き、Vue Routerについての記事となります。

ky-yk-d.hatenablog.com

Vue Routerでアクセス制限を実現する

今回は、ナビゲーションガードを利用したアクセス制限を実現するコードを解読していきます。実現したいのは、「特定のパスに対してアクセス制限をかける」ことです。

今回は、コードの中核部分を『基礎から学ぶVue.js』(およびそのサポートページ)に負っています。初心者にわかりやすく、また実践的でもあり、そしてサポートページが実に充実している書籍です。ありがたい。

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

今回のコードを置いているGitHubリポジトリはこちら(すでに更新が加わっています)。このリポジトリに限らず、コードの書き方等に指摘あればぜひお知らせください!

github.com

Vue Routerの設定とナビゲーションガードの導入

前回、Vuer Routerの設定は以下のようにしていました。

let router = new VueRouter({
  routes: [
    {
      path: '',
      component: Top,
      children: [
        {
          path: '',
          component: ChildA,
          name: 'childA'
        },
        {
          path: '/childB',
          component: ChildB,
          name: 'childB',
          meta: {
            requiresAuth: true
          }
        }
      ]
    },
    {
      path: '/helloworld/:msg',
      component: HelloWorld,
      name: 'helloworld'
    }
  ]
});

/childBというパスに対して、meta要素にrequiresAuth: trueを指定しています。この要素を利用して、認証が必要なパスと不要なパスとを区分していきます。

ナビゲーションガード(グローバルガード)のコード

ナビゲーションガードを導入します。利用するのは「グローバルガード」です。

今回記載したソースコードは下記のようなものです。こちら、ほぼ『基礎から学ぶVue.js』記載のコードとなっています。

router.beforeEach((to, from, next) => {
  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!store.state.isLogin) {
      next({
        path: '/',
        query: {
          redirect: to.fullPath
        }
      })
    } else {
      next();
    }
  } else {
    next(); 
  }
});

このコードを読み解いてみます。

Vue Routerの様々なオブジェクト

router.beforeEach()は、ルーターインスタンスのメソッドで、画面遷移前に実行される処理を引数に記載します。処理の中では、遷移先をnext()で指定します。上の例の場合、一定の条件下でリダイレクトさせるために利用しています。

次に、ifの条件となっているto.matched.some(record => record.meta.requiresAuth)についてです。toは、遷移先を示すルートオブジェクトです。単なる文字列ではありません。公式ドキュメントの記述を引用します。

遷移先のURLの、現在の URL をパースした情報と、その URL とマッチしたルートレコードを保持しています。ルートレコードはroutes設定の配列 (とchildren配列) 内のオブジェクトのコピーです。

matchedは、ルートポブジェクトのプロパティで、この「現在のルートのネストされた全パスセグメントに対しての ルートレコード を保持している配列」です。

この場合、VueRouterのコンストラクタに渡しているオブジェクトのroutes配列の1つ目の要素(path: ''の部分)と、その中のchildren配列の2つ目の要素(path: '/childB'の部分)とを要素とする配列ということになります。

Array.prototype.some()

そして、some()メソッドはArray.prototype.some()です。

developer.mozilla.org

some()メソッドは、to.matchedという配列の各要素をそれぞれrecordとして扱い、record.meta.requiresAuthが真となる要素があれば真を返します。今回の場合、childB側の要素が条件に合致するため、some()メソッドが真を返し、認証の確認部分が実行されるというわけですね。

※今回の例では、requiresAuth: trueと指定しているchildBがネストの最下位ですが、some()を利用することで、上位のパスにrequire sAuth: trueを指定されていれば認証確認の対象となります。

認証の確認とリダイレクト、そしてVuexへ

認証確認部分はシンプルです。if(!store.state.isLogin)でログインしているかどうかを確認し、ログインしていなければnext()の引数(ルートオブジェクト)で認証不要なパスを指定します。上記の例では、クエリ文字列でリダイレクト元(本来の、許可されなかった遷移先)の情報を付加しています。

f:id:ky_yk_d:20180707184943p:plain

以上によって、「特定のパスにアクセス制限をかける」ことが実現できました。そのまま使えるサンプルコードや、親切な解説書の存在はとてもありがたいですが、その先で自力でAPIドキュメントを読んでみると、理解度が全然違いますね。Array.prototype.some()も、今回はじめて知りました。

今回は、Vue Routerのナビゲーションガードを用いて特定のパスにアクセス制限をかけてみました。今回用いたのはグローバルガードと呼ばれる最も基本的なナビゲーションガードでしたが、他の種類のナビゲーションガード、あるいはナビゲーションガード以外のVue Routerの機能も少しずつ試していきたいと思います。

また、ちらっと登場したstore.state.isLoginは、実はVuexを利用しています(アクセスの仕方がこれでいいのかは不明・・・)。まだVuexは消化不良なのですが、ある程度の段階で記事にまとめようと思います!

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

Vue Routerで「ネストされたルート」を試した

久しぶりにVue.jsの記事です。基本的な機能は使えるようになってきているので、Vue Routerに手を伸ばしてみました。

紹介 | Vue Router

Vue Routerを試そう

Vue Router自体は、以前ご紹介したUdemyの"The Ultimate Vue JS 2 Developer Course" (結局最後まで終えていません・・・)のなかで利用したことがあります。

ky-yk-d.hatenablog.com

この時は、基本的な画面遷移の機能と、パラメータを渡す「動的ルートマッチング」の機能を利用していましたが、今回「ネストされたルート」の機能を試したのでご紹介します。GitHubリポジトリは下記。

github.com

ネストされたルートとは

ネストされたルート | Vue Router

f:id:ky_yk_d:20180701214153p:plain
公式ガイドより

ネストされたルートは、上の公式ガイドに記されているように、ネストされたコンポーネントの特定の構造に対して別個のURLを割り当てるために利用する機能です。

単純な<router-view><router-link> では、それが設置された直下のコンポーネントを丸ごと制御することしかできません。上の例でいえば、Userコンポーネント丸ごとを扱うことしかできません。もしProfileコンポーネントとPostsコンポーネントに別個のURLを割り当てようとすれば、Userコンポーネントを固定しなくてはなりません(そのレベルの切り替えはできなくなる)。

実装してみる

今回、この機能を利用した例として、Topコンポーネントの配下にChildAあるいはChildBというコンポーネントが入るネスト構造にURLを割り当てる画面を作成してみました。

TopコンポーネントはさらにAppコンポーネントに属しているため、App.vue内で下記のようにVue Routerを設定します。

App.vue

let router = new VueRouter({
  routes: [
    {
      path: '',  
      component: Top,
      // ここにname要素を指定するとエラーになる
      children: [
        {
          path: '', 
          component: ChildA,
          name: 'childA'
        },
        {
          path: '/childB', 
          component: ChildB,
          name: 'childB',
          meta: {
            requiresAuth: 'true'
          }
        }
      ]
    }
  ]
});

Top.vue

<template>
  <div id="top">
    <h2>Hello Vue Router World!</h2>
    <router-view></router-view>
  </div>
</template>

以上のように設定しておくと、トップページにアクセスした場合にはTop配下にChildAコンポーネントが表示されます。言い換えれば、App.vueの中にある<router-view>にはTopコンポーネントが、そしてTop.vueの中にある'<router-view>`にはChildAコンポーネントがそれぞれ入ることになります。

つまり、この構造に対して、''というパスが割り当てられているということになります。<router-link>で名前を用いる場合には、nameで指定したchildAでアクセスすることができます。

http://localhost:8080/#/

f:id:ky_yk_d:20180701215608p:plain

一方、[Top-ChildB]というまとまりに対しては、'/childB' というパスが割り当てられ、名前はchildBとなります。下記のスクリーンショットから、Topコンポーネントの配下にChildBコンポーネントが配置されていることがわかると思います。

http://localhost:8080/#/childB

f:id:ky_yk_d:20180701215520p:plain

注意点

[Top-ChildA]というまとまりに対してパスを割り当てているため、routerの設定でTopコンポーネントのレベルにname要素を指定しようとするとエラーとなります。これについては、うっかり指定してしまった場合はエラーメッセージで教えてくれるので、それほど困らないポイントかと思いますが、最初気づかなかったので書き残しておきます。

感想

簡単にネストされたルートが使えることがわかりました。ネスとされたルートが使えると、実装できるページの幅も広がりますね。Vue Router、すごい!

Vue Routerのその他の機能(ナビゲーションルートなど)も、少しずつ使ってみています。コンポーネントをバシバシ使うようになると、アプリケーション全体での状態の管理が煩雑になってきているなと感じます。今回実装してみた中でも、フラグ(ログインの有無)を用いてアクセス制限をしてみているのですが、ひどい実装になってしまいました。

というわけで、次はVuexを導入してみようと思います。ゆっくりとした歩みではありますが、2ヶ月前(↓)からは随分進歩しました(笑)途中になっているUdemyの講座もあることですので、これから1,2ヶ月でVueについては「一通りわかった」と言える状態を目指します!

ky-yk-d.hatenablog.com

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

6/26 「カイゼン・ジャーニー・ライトニングトークス」で初LT登壇した

本日、「カイゼン・ジャーニー・ライトニングトークス」というイベントに参加してきました。勉強会には足繁く参加している僕ですが、今回が初めてのLTとなりました。

devlove.doorkeeper.jp

カイゼン・ジャーニー』

イベントの内容は、書籍『カイゼン・ジャーニー』にまつわる内容のLT大会です。この書籍は僕の背中を強く押してくれた本なので、「『カイゼン・ジャーニー』に背中を押されて行動したよ」という話であればそこまで悩むことなくできたと思います。ですが、せっかくの機会に書籍の礼賛をして終わっても勿体無いですし、その手の話をしても「良かったね」という感想しか持ち帰ってもらえないと考え、最近よく考えていることとカイゼン・ジャーニーを結びつけてお話しすることにしました。

発表資料

忘れられがちな気がする「文字言語」の重要性

今回のLTでは、「文字言語」の重要性についてお話ししました。これは、自分がここ数ヶ月、社内でアイデアの導入のために試行錯誤をしている中で、「明確に人に考えを伝えることに失敗しているな」と感じたところから出発しています。

長い文章を書くということは、エンジニアの世界ではあまり重要視されないというか、そもそも業務として少ないと感じています。文章らしい文章が書かれるとすれば、障害報告書くらいでしょうか(書いたことありません)。エンジニアにとって身近なのは、やはり顔を合わせての口頭での会話や、Slackなどでのライトなコミュニケーションなのだと思います。

もちろん、面と向かってのコミュニケーションでよく伝わることもあると思います。不安や感謝といった感情に関わるものは、かっちりとした文章よりもそのようなコミュニケーション方法によってこそ伝わるものでしょう。そして、それは行動によって裏付けられ、人を巻き込んでいくものです。

しかしながら、明晰な言語で論理的に綴られた文章は、自分の考えを伝えるためには、依然として極めて強力なツールなのではないか、と思っています。自分が認識している現状や、ある行動の根拠を精密に伝えようとすれば、それが複雑であればあるほど、言葉を尽くして説明する必要があります。それができるのはやはり、文字言語による文章である。それが発表の趣旨でした。

カイゼン・ジャーニー』と「アジャイルマニフェスト

僕はこの話をしようと思ったとき、当初は『カイゼン・ジャーニー』とは違う方向に行きそうだなと思いました。なぜなら、『カイゼン・ジャーニー』は行動を重んじる書籍だと思っていたからです。

カイゼン・ジャーニー』は、モヤモヤを持ちながらも燻っている人の背中を押してくれる書籍です。石神の「あなたは何をしている人なんですか」という問いは、多くの読者に強く響いたのではないでしょうか。「文句ばかり言っていないで行動を起こすことが重要だ!」というメッセージを受け取った僕は、考えたことを行動に移してきました。

しかし、僕はおそらく カイゼン・ジャーニー』を誤読していた のだと、今は思っています。なぜなら、あの書籍には文字言語による「見える化」を利用するプラクティスも多く含まれていたからです。長大な文章を書こうという話は出てきませんが、そもそもあの書籍自体が文章です。物語は明晰さとは縁がないように思われるかもしれませんが、江島の目の前に横たわっていた現実の描写や、プラクティスの解説の存在によって、あの書籍は単に情緒に訴えるだけのものに留まっていないと思います。

確かに『カイゼン・ジャーニー』は、情動に訴えかけて行動を促す要素のある、行動を重んじる書籍だと思います。しかしそのことは、あの書籍が 言語を軽んじるものであることを意味しません。 それは、「アジャイルマニフェスト」がドキュメントや計画の価値を貶めているわけではないのと同じなのです。

文章を書いていくという所信表明

本来であれば、自分が実践していることを発表すべきところなのですが、正直に申し上げて、僕はまだ自分の考えを正しく伝えるような文章を書けていません。そしてそれは、文章になっていないだけではなく、まだ思考の上でも曖昧なままなのです。

もちろん職場に直結するような内容は、ブログに書けることではありません。ですが、それは文章を書かない理由にはなりません。僕が取り組もうとしている切実な課題がそこにあるのであれば、公開できるできないに関わらず僕は書くべきなのだと思います。

数ヶ月後、数年後、僕にとって記念すべき初めてのLTとなったこの発表をふりかえって、「あのときの自分は間違っていなかった」と思えるかどうか。それは予想できません。ただわかるのは、「書いてみなければ答えはいつまでも得られない」ということです。こればかりは、 書くということを実際に行動に移して初めてわかる ことです。

最後の最後で、行動と言語の二項対立が崩れてしまいました。明晰さの欠片もありませんね。このような文章を書かないよう、みなさんもこれから言語を鍛えていきましょう。

末筆ながら、主催者のお二人、参加者のみなさま、ありがとうございました。

カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで

カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで

van-thks.com

環境構築弱者でも簡単に始められるテスト駆動開発〜mocha + power-assert でJavaScriptのテストを書く〜

環境構築弱者でもテスト駆動開発がしたい!

JavaScriptテスト駆動開発を始めてみる

みなさん、テスト書いてますか?僕は書いてません。

f:id:ky_yk_d:20180623082423p:plain

はい、すみません。

というわけで今回は、JavaScriptテスト駆動開発を始めてみたという記事になります。具体的には、

テストを実行するための、環境構築とテストの書き方をまとめました。

power-assert.jsは、和田卓人(@t_wada)さんが作成したライブラリです。その特質は下記のスライドをご覧ください。

www.slideshare.net

mocha.jsのGitHub

github.com

power-assert.jsのGitHub

github.com

【補足】「環境構築むずかしいのでは?」とブレーキをかける認知を補正する

最初、普通に環境構築してテスト書いて〜〜という記事を書いていたのですが、この記事を書くモチベーションについて思うところがあったので少し文章を。

テスト駆動開発に限らないのですが、何か新しいことを始めるときに、環境構築で転ける経験をしている方は多いのではないでしょうか。僕もそうで、いくつか転けてきた経験により何を始めるにも「よくわからないけど環境構築むずかしそう・・・」と二の足を踏んでしまうことがよくあります。

しかし、すでに始めている人からすると実は簡単なんだよというものは多いんじゃないかとも思っていますし、僕自身、何らかのきっかけで踏み出してみて、「なんだ、意外と簡単じゃん」と感じることもあります。

今回に関しても、「やってみたら意外と簡単だった」というのが結果としての感想なのですが、そういう記事が増えてくれるとどんどん新しいことを試していけるなぁと思いまして、とりあえず過去の自分に対しては「簡単なんだよ」と言えるように今回の記事を書きました。

そういうわけで、テスト駆動開発の中身(レッド・グリーン・リファクタリング)については特に記載していません。t_wadaさんの下記スライドをご覧ください。

環境構築

こちらの記事を参考にさせてもらいました。

コマンドラインからの環境構築

npmは入っている前提です。ちなみにMacです(たぶん関係ないと思う)。

cd [プロジェクトのディレクトリ]
npm init
mkdir test
npm install --save-dev mocha # たぶん
npm install --save-dev intelli-espower-loader # この辺は 
npm install --save-dev power-assert # 一気にやっていい

以上でコマンドラインからの環境構築は終了です。少し前まではnpm installと出てくると頭が痛かったのですが、VueやらWebpackやらと戯れているうちに大分慣れてきたように感じます。

npm run mochaで実行できるようにする

上記の設定だけを行い、テストを実行するコマンドmocha --require intelli-espower-loaderを叩いたところ、「mochaなんてコマンド知らねーよ」と怒られてしまいました。--save-devオプションをつけたmochaにはパスが通っていないので、mochaコマンドでは呼べません。

この事象に対処する一つの手段は、npm install -g mochaを実行することです。グローバルインストールすることで、パスが通ってmochaコマンドが使えるようになります。しかし、何でもかんでもグローバルインストールするのがダメだということは何となくわかります。

というわけで、下記の記事を参考にpackage.jsonを修正し、ローカルのものを使用するように変更しました。

package.json

{
  .
  .
  .
  "scripts": {
    "mocha": "mocha --require intelli-espower-loader"
  },
  .
  .
  .
}

以上の設定をすることで、npm run mochaでテストを実行することができました。いちいちオプションを指定しなくてもいいのも便利ですね。これで環境構築はできました。意外と簡単ですね。

※グローバルインストールしたmochaをnpm uninstall -g mochaでアンインストールしてみたところ、mochaコマンドは使えなくなりましたが、npm run mochaは問題なく使えたので意図した通り、ローカルのmochaを使えるようになったようです。

テストを書いて実行してみる

どう書くのか/どんな表示が出るのか

いよいよテストを書いて実行していきます。JavaScriptも初心者なのでツッコミどころがあったらGitHubなりTwitterなり(匿名がいい方は)質問箱なりに投げていただければ幸いです。

全て3の配列の2番目の要素(=3)と、1~5までの配列の2番目の要素(=2)を比較するテスト(落ちるはず)を書きました。下記の2つのコード(実装/テスト)を作成し、npm run mocha ./test/sample-test.jsコマンドを叩きます。さぁ、どんな結果が出るでしょうか。

./src/sample.js

exports.getActual = () => {
  let actual = [];
  [...Array(5)].map((_,i) => {
    actual.push(i + 1);
  });
  return actual;
};

./test/sample-test.js

const assert = require('power-assert');
const myModule = require('../src/sample');

describe('サンプルテスト', () => {
  it('落ちるテスト', () => {
    let expected = Array(5).fill(3);
    assert(myModule.getActual()[1]=== expected[1]);
  });
});

実行結果

f:id:ky_yk_d:20180623121041p:plain

想定通り落ちました。そして、「何が起きたのか」がコンソールに表示されています。t_wadaさんが「すごい表示、素晴らしい表示、実にわかりやすい表示」自画自賛する表示です。

単純に想定と実際が出てくるだけでなく、assert()の引数に渡した式の途中の評価結果も表示されています。これなら、どの時点でおかしくなっているのかもよくわかります。便利に使えそうですね。

-w オプションで変更を検知してテストを自動実行する

ちなみに、npm run mocha -w ./test/sample-test.jspackage.jsonscripts "mocha": "mocha --require intelli-espower-loader -w"-wオプションをつけると、ファイルの変更を検知してテストを自動実行するモードになります。このモードを終了するにはctrl + cです。下記の記事を参照しました。

毎回コマンド叩く必要がなくなるのでサイコーです。

2018.06.24 修正

"mocha": "mocha --require intelli-espower-loader"の設定下で、変更監視モードで起動するには、 npm run mocha -- -w ./test/sample-test.jsなどと--`を付けてからオプションとファイル名を記載する必要がありました。下記の記事を参照しました。

まとめ

以上、環境構築方法とテストの書き方をご紹介しました。簡単に始められたので、ガシガシテスト書いていこうと思います。

  • mocha + power-assert のテスト環境は簡単に構築できる!
  • power-assert は便利に使えそう!
  • -w オプションを使えばさらにテストは高速に!

参考記事一覧

テスト駆動開発

テスト駆動開発

【おまけ】素因数分解プログラムのテストを書いてみる

整数値を渡すと素因数分解した結果を配列にして返すプログラムを書いてみました。JavaScriptよくわからん。

なお、開発の過程はブログ向きでないので、GitHubリポジトリをご覧ください。コミットの単位が荒くて恐縮ですが、試行錯誤の跡を見ていただけるかと思います(笑)

github.com

実装コード

let isDivisor = (dividend, divisor) => {
  return dividend % divisor === 0;
}

exports.findSmallestFactor = function(num){
    let possibleLargestFactor = Math.floor(Math.sqrt(num));
    for (let i = 2; i <= possibleLargestFactor; i++){
      if (isDivisor(num, i)){
        return i;
      }
    }
    return 0;
  };

 exports.factorize = function(num){
    let result = [];
    let temp = num;
    while (true){
      let smallestFactor = this.findSmallestFactor(temp);
      if (smallestFactor === 0 ){
        result.push(temp);
        break;
      } else {
        result.push(smallestFactor);
        temp = temp / smallestFactor;
      }
    }
    return result;
  };

テストコード

const assert = require('power-assert');
const PrimeFactors = require('../src/PrimeFactors');

describe('PrimeFactors', () => {

  describe('素因数分解が正しく行われる', () => {

    it ('2のときは2を返す', () => {
      assert(PrimeFactors.factorize(2).toString() === [2].toString());
    });
    
    it ('4のときは[2,2]を返す', () => {
      assert(PrimeFactors.factorize(4).toString() === [2,2].toString());
    });
    
    it ('6のときは[2,3]を返す', () => {
      assert(PrimeFactors.factorize(6).toString() === [2,3].toString());
    }); 

    it ('108のときは[2,2,3,3,3]を返す', () => {
      assert(PrimeFactors.factorize(108).toString() === [2,2,3,3,3].toString());
    });

  });

  describe('最小の約数を返す', () => {

    it ('12を渡すと2を返す', () => {
      assert(PrimeFactors.findSmallestFactor(12) === 2);
    });
    
    it ('17を渡すと0を返す', () => {
      assert(PrimeFactors.findSmallestFactor(17) === 0);
    });

  });
  
});

実行結果

f:id:ky_yk_d:20180623112659p:plain

まさに「何もなければ黙るのみ、落ちるときはやかましく」ですね。

F.O.X Meetup #3 ~スタートアップのチームビルド~ に参加した

F.O.X Meetup #3 ~スタートアップのチームビルド~

6/18に開催された「F.O.X Meetup #3 ~スタートアップのチームビルド~」(@ヒカラボ)に参加してきました。

career.levtech.jp

カカカカックさんのプレゼン姿を見たかった & チームビルドに関心があった

現在、このブログを中心としたアウトプットについてのメンタリングを、カカカカック(@kakakakakku)さんにしていただいています。日頃のTwitterでのやりとり以外に、ブログやプレゼン資料、ゲスト出演されたポッドキャストについては拝読・拝聴していましたが、直にお会いしたことはなく、プレゼンという形のアウトプットに自分の目と耳で接したことがなかったので、いい機会と思い参加しました。

内容面でも、チームビルドという非常に関心を持っている分野について、スタートアップで働かれているお三方からお話をうかがえるということで、非常に楽しみにしていました。

資料はお三方ともに後日公開、かつメディア取材が入っていてそちらの記事が各登壇者についてそれぞれ1本(!)公開される予定とのことでしたので、内容についてはあまり触れず、感想を。

(随時追加)資料等

・門田さんの資料

www.slideshare.net

・田中さんの資料

www.slideshare.net

・カカカカックさんの資料

kakakakakku.hatenablog.com

・門田さんの講演についての記事

logmi.jp

・カカカカックさんの講演についての記事

logmi.jp

門田 矩明 さん「成功したチーム、失敗したチーム」

F.O.Xの門田さんです。F.O.X Meetupの主催者の方。

cyber-z.co.jp

これまでのキャリアで経験された3つのチームの事例をもとに、成功するチームと失敗するチームを分けるものは何かのか、というテーマでお話しされました。

結論としては、スタートアップで成功するのは、

  • チーム内でタスクや知識をシェアできる
  • 専門領域を超えた仕事ができる
  • つまり、いわゆる「チームワーク」のできている

チームだというお話でした。この結論自体は、ありふれたものでしたが、お話を拝聴していて感じたことがあったので書き留めておきます。

時期によるタスクの偏りという問題

2番目のチームとして、アドテク製品のフルスクラッチでの開発において、4つのサブシステムにそれぞれ専任者を付けて開発を進めた結果、「失敗したチーム」となってしまったチームのお話がありました。

発生した問題は、サブシステム間(すなわちメンバー間)でタスクの偏りが生じた際に、手すきの人員に手伝わせようとしても、専任制でノウハウが共有されていないためにキャッチアップできない、ということでした。

誰も専門的な知識を持っていない領域に対する調査〜開発のあり方

3番目のチームとして、2012年ごろに携わったスマホアプリの開発についてのお話がありました。当時、スマホアプリ開発は現在ほど広く普及したものではなく、そこについての情報が少ない、専門的な知識を持ったエンジニアがチームにいないという状況で、どのように調査・開発を進めたか?というお話でした。

要点としては、

  • 専門領域を問わず複数名で技術調査する
  • 調査メンバーで全領域を設計し、そのメンバーがコードレビューする
  • 調査〜開発までを複数名で実施する

という取り組み方をした結果、ドキュメントに書いていないことが起きるなどのトラブル発生時にも複数名で対応でき、プロジェクトは成功したということでした。

全員が未知の領域の場合、担当を割り振ってまずはその人にチーム内での第一人者になってもらうという行動に出たくなってしまいがちであるように思います。しかし、結果として後からキャッチアップするのに手間がかかる、レビューが正常に機能しなくなるなどの問題が発生するリスクはやはり大きいなと感じました。

田中 裕一 さん「タイムバンクでみるプロジェクトの立ち上げとMVP」

タイムバンクの田中さん。「見た目のせいか『デザインもやって!』と言われますが、デザインは全然できません」とのお言葉が印象的です。

timebank.jp

冒頭で予告された内容は以下のような点でした。

  • クラッチから立ち上げてどのような苦労があったか
  • チームワークをどのように作っていったのか
  • エンジニアの組織で起きがちな問題

拝聴していて印象に残ったことをいくつか。

各フェーズにおいて何を諦め、何を重要視するのか?

「有益な施策はたくさんあるが、それを一時期にすべて実施するのは無理」という厳然たる事実に基づいたお話でした。本を読んだりネットの記事を読んだりすれば、「こうすれば良くなる」という施策は技術の導入であったりプラクティスであったり様々なものを知ることができます。僕自身、日々そういうものを学び、「あれもやってみたい、これもやってみたい」と半ばワクワク、半ば焦燥に駆られています。

しかし、当然ながら「何ができて何ができず、何が有効で何が効果薄なのか」というのは、現場や時期によって異なることです。これは当たり前の事実で、様々なプラクティスを紹介している『カイゼン・ジャーニー』でも冒頭(第1部第1話の解説)にも似たような趣旨のことが書かれています。

私たちは、他者の実践の背景にどんな状況、制約があったのかを理解し、自分たちの状況、制約の下ではどのように実践するべきなのか捉え直さないといけない。(『カイゼン・ジャーニー』13頁)

今回の田中さんのお話は、ある現場において、立ち上げからフェーズを切りながら、それぞれのフェーズで可能かつ有効な施策を考え、実践していった記録として非常に面白く聴きました。

チームとしての取り組み

技術的背景も、価値観もバラバラなメンバーからなるチームにおいて、どのように行動をしていったかをお話しされていました。印象に残ったもののみ挙げておきます。発言ベースなので内容に重複があります。

  • お互いの「正義」が違うことを理解する
  • 「開発チームとして、何を優先し、何を諦めるのか?」を合意し、それを時期に応じて変えていく
  • 自分たちの現状と向かうべき先について共通理解を作る、そのためには時間を取る
  • 一度決めたことは当面はブレさせない
  • チケットには最低限、いつ、何が起きて、誰が対応したのか?だけはわかるように書く(初期の妥協点)

MVP & the Wizard of Oz

MVPのお話。書籍で読んで知ってはいましたが、実際に適用されている方のお話を伺うのは初めてでした。下記のような発言がありました。

  • KPIの跳ね方を見てピボットするなどの行動をとる
  • 仮説と検証を回す
    • 検証不可能なものは検証可能な仮説にするか、聞かなかったことにする
    • 心理的表現が含まれる仮説はボケたりするのでコストばかりかかることが多い
  • ユーザ体験を損ねる方向のMVPの使い方は危険

吉田 慶章 さん「さぁ!今すぐプロジェクトリーダーに立候補しよう」

大トリはカカカカックさん。下記のブログで公開している社内勉強会の内容をアップデートしたものをお話してくださいました。

kakakakakku.hatenablog.com

冒頭に、「資料を後日公開するのでメモはとらずに僕の話を聴いてください」と通告されました。スライドになかった発言だけメモしていましたが、ご本人が終了後に「めっちゃアドリブで喋っちゃう」と仰っていましたので、ちょうどよかったなと思いました。

長いプロジェクトは失敗する?

今回、章立てとしては、

  • 「タスクの流れ」に着目する
  • 「雑談」と「スウォーミング」を大切に

の2つに分けてお話をされたのですが、これらに入る前にされた「プロジェクトとは?」というお話の中で、下記のような趣旨の発言がありました。

4ヶ月以上のプロジェクトは切り分け方が悪い。絶対失敗する。3ヶ月くらいで何かしらリリースすべき。

僕はプロジェクトの経験があまりないので、このあたりの肌感覚はまだわからないのですが、おそらくタスクと同じで、長い間、目に見える成果を提出できないというのは辛いし、行き詰ってしまいがちなんですよね。リリースを文字通りに捉えるとスタートアップ限定の話に聴こえますが、一般(?)のプロジェクトも短く切れたほうがいいのかなと感じました。

「タスクの流れ」に着目する

人を抜き取られてしまわないために、うまく動いてることをスプリント計画を使って示す

スプリント計画は経営層やステークホルダーに対しても有効という視点は考えたことがありませんでした。人を抜き取られたり追加されたりするのはリスクなので、チームを守るためにはかなり大事ですね。

スキルマップの△は、次のプロジェクトでは◯になっていてほしい人

プロジェクト自体の成功とは別に、個人の望む成長も実現したいというのはプロジェクトに関わる人全員に共有されてほしいです。。

「雑談」と「スウォーミング」を大切に

後半は、話の内容よりもプレゼンの仕方の工夫が印象に残りました。

「3秒休んで一呼吸おき」、後半に突入

「3秒休みます」と声に出して仰っていました。切れ目に間を作るというのはよく言われますが、「休みますよ」と言われるとたとえそれが3秒でも「あ、いま気を抜くタイミングだ」と安心して気を抜けるので良かったです。

スライドを映して上から下まで読ませてから、口頭での説明・ストーリーを追加する

スライドを読むだけのプレゼン辛いというのはよく言いますし、このやり方は珍しいものではないと思いますが、視覚と聴覚の両方をうまく使ってプレゼンを「体験」することができるなーと感じました。

終わりに

カカカカックさん目当てで参加したイベントでしたが、とても満足感が高かったです。普通なら、余韻を楽しんで終わってしまうところなのですが、カカカカックさんから下記のツイートが飛んでくるのでブログ書きました。

というわけで、これにて勉強会終了です!

カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで

カイゼン・ジャーニー たった1人からはじめて、「越境」するチームをつくるまで

ザ・ゴール ― 企業の究極の目的とは何か

ザ・ゴール ― 企業の究極の目的とは何か

カンバン仕事術

カンバン仕事術