toggle Engineer Blog

トグルホールディングス株式会社のエンジニアブログでは、私たちの技術的な挑戦やプロジェクトの裏側、チームの取り組みをシェアします。

街の今と未来をデジタル空間上でなめらかに繋ぐ: トグルがつくる新しいデジタル産業インフラの姿について

はじめに

トグルホールディングスの新谷です。AI開発の責任者をしています。

「不動産」「建築」「金融」というまちづくりの3大要素を、AIを活用してひとつの統合基盤へと結実させる――。 私たちトグルホールディングスは、かつて存在しなかった新種の「デジタル産業インフラ」を創り上げることをミッションとするスタートアップです。

トグルのアドベントカレンダー2024に記載の、数々の技術検証やプロダクト開発の要素たちは、すべて、トグルがつくるデジタル産業インフラの構築に向かって走ってきた軌跡です。

私たちが向かう世界では、不動産・建築・金融を取り巻く情報空間が、どのような形を成しているのか、その紹介ができればと思います。

(元記事:PLATEAU ADVENT CALENDAR 2024 すべての不動産・建築コンテンツはデジタルツインに統合される

地図が「地球」を映しはじめた

世の中に起きている変化として、近年、地図アプリケーションは飛躍的な進化を遂げています。 これは、デバイスや通信技術に加え、データのクオリティ、さらにはアプリケーションの発展の影響が大きいです。 もはや地図は、原始としての「位置関係図」から「地球を映す立体情報空間」へと大きく変貌を遂げてきています。私たちはデジタルツインをこのように捉えています。

デジタルツインの発展により起きる変化

これまで、不動産といえば地図、建物といえば図面、いった形でバラバラに管理されていました。 ただ、不動産や建築の情報は必ず、ある「場所」に依拠しています。 この事実が意味するのは、今来のデジタルツインの発展により、不動産・建築の情報は、それぞれ高次元のデータのまま、地図上に同居していくことが予想される、ということです。

敷地にかかる法令制限の確認や、地盤・高さの把握、さらには建築設計まで、全てが地図上で行われる未来も近いと考えます。

これら変化は、私たちのミッションである、「不動産・建築・金融をひとつにまとめる新しいデジタル産業インフラをつくる」の道中に起きている必然的な出来事だと言えます。

トグルのプロダクト

私たちは、「街の“未来”を描く地図」を開発しています。

plateau_mini.gif

vc_lite.gif

「3D地図を上空から眺め、都市の概況を把握。開発余地のある領域を見つけ、その場に建つべき建物をAIで計算する。それは街の”未来”を見ることだ」
フル動画は こちら

コア技術となるのは、建築基準法を遵守した最大・最適な建物プランを自動作成するAI技術です。 この技術によって、デジタルツインという場で、街の現在と未来をなめらかに繋いでいます

Plateau_digital_twin.gif

このように、PLATEAUデータをはじめ、国土交通省国土地理院など、さまざまなGISデータや3Dデータを取り込み、独自のAI技術と掛け合わせることで、抜本的に新しいプロダクトを生み出しています。

まちづくりにおける、情報流路のインフラをつくる

建築は、単純に「建物をつくる」行為ではありません。 それは「街」という巨大な情報空間の中から条件を導き、法や条例に適合させながら、人間や自然の未来の営みをデザインする、極めて複雑なプロセスです。

新しい建物が街に建っていくーー。 このプロセスの中では、街の情報空間と、人の思案に基づく未来のデザイン、そしてリアルを、何度も行き来します。 私たちが目指すデジタル産業インフラは、このサイクルをなめらかにつなぎ、都市開発や不動産業務を一元的かつ合理的に進めるための「流路」となることを見据えています。

最後に

もしこの世界観に共鳴していただいた方は、ぜひ一度ご連絡ください。 私たちが創ろうとしているのは、まったく新しいデジタル産業インフラです。 共に、すべてのまちと、まちをつくる人たちの”未来”を描いていきましょう。

MISSION: すべてのまちと、まちをつくる人たちのために

採用情報はこちら

入社して約一年半のGISと地図技術への取り組み、そして今年参加して良かったGISイベントを振り返る

こんにちは。トグルホールディングス プロダクトエンジニアの尾形 ( id:xtetsuji ) です。 トグルホールディングスエンジニアアドベントカレンダーの24日目の記事です! 2024年もあと7日ほど。早いですね。

2024年のトグルホールディングスを振り返ると、今年始めての試みとして、各種カンファレンスへのスポンサードを行いました。5月の TSKaigi #1(東京都中野区)、11月の TSKaigi Kansai京都府京都市) には Platinum Sponsor として参加しました。また 10月に行われた YAPC::Hakodate 2024(北海道函館市)にも Perl Sponsor として参加しました。

TSKaigi へのスポンサードからも分かる通り、トグルホールディングスが重点的に取り組んでいるの重要技術の一つとして TypeScript が挙げられますが、その他にもいくつか重要技術があります。生成AIといった各所で取り組まれているものもその一つですが、とりわけ不動産業務と切っても切り離せないのが 地図に関する技術GIS と呼ばれたりもするものです。

GIS とは

GIS (ジーアイエス) とは Geographic Information System の頭文字を取った略語で、日本語では 地理情報システム などと訳されます。簡単に説明すると、緯度経度といった場所に紐づく情報=地理空間情報を取り扱う技術の総称のことです。Wikipedia にある「地理情報システム」の記事が詳しいです。

今までに公開された toggle holding アドベントカレンダー2024 の記事でも、GeoJSON や PLATEAU といった GIS にまつわるトピックを取り扱っています。

GIS が対象とする領域は地図にとどまらないのですが、地理空間情報のプロット手法として地図が用いられることは自然なことで、私は GIS という単語を地図技術の類義語として語ってしまうことがあります。

私がトグルホールディングスに入社したのは2023年の夏だったのですが、その 入社動機は「業務の中で興味があった地図技術を学んで実践できる」 ことでした。

私と地図

2020年のコロナ禍で満足に外出できない日々が続いた時、自宅でよく Google MapsGoogle Earth を開いて色々な場所を見て「バーチャル旅行」をしていました。

元々、行ったこと無い場所を地図で見て何があるか想像したり、昨今のWeb地図でリアルタイムに現在の状況が反映された地図を眺めるのが好きで、日常的に Yahoo!マップや地図マピオンなどの iOS/iPadOS アプリを取得したい情報に応じて使い分けたり、公共交通機関のリアルタイムな動向を Mini Tokyo 3DFlightradar24 で確認したりしています。位置情報サービスの草分け的存在である Foursquare / Swarm はサービス開始当時からのユーザです。

ユーザとして複数の地図系アプリの熱心な愛用者ではあったのですが、ウェブ開発者としては「こんなアプリ作れたらいいなぁ」と思いつつ、プライベートな時間でゼロから何か作るほどの行動力を発揮できずにいました。勉強しない言い訳は色々できますが、「仕事にしてしまえば平日ずっと向き合える」という単純な発想から2023年に転職。やる気だけあって(GISに関しては)専門知識がない私を「地図担当」として採用して頂いたことは有り難い限りです。

業務で触れたGISとWeb地図

2023年夏の入社後は、土地仕入れを効率化する社内アプリケーション TSURUHASHI の開発・保守運用の担当となりました。

TSURUHASHI については下記の記事を参照ください。

すでに一定の完成を経ていた TSURUHASHI にさらなる改善を施すべく、フロントエンドに詳しい方の力を借りながら、主にバックエンドの GIS データの整備を中心に行っていました。

当時は体系的な GIS の勉強をしたかったものの、どこから着手していいかわからず、とりあえず業務で出会ったものを都度調べていました。シェープファイルGeoJSONgeometry型、ST空間関数、空間JOINなどの基本的な(今では基本的とわかる)ものから、プロジェクト固有の概念まで、今まで出会うことがなかった GIS の技術要素に出会い学んでいきました。

入社当時はプロジェクトメンバーが大きく変わっていた時期で、気軽に質問できたりと頼れる人が一時的に激減していたこともあって学びに苦労しましたが、半年ほど GIS を学んで俯瞰ができるようになったことで、GIS 初学者への勉強のロードマップをある程度示すこともできるようになりました。私自身、学習曲線がなかなか上向かない人ではありますが、エンジニアが一気に増えた2023年末から新卒社員が入社した2024年春以降に適宜アドバイスができたことを思うと、自分の入社時期は良かったなと思います。

なお 2024年12月現在、GIS 初学者が一通りの GIS の知識をつけるには、教科書として「位置エン本」「位置ベロ本」を最初に一通り読んでもらうことが一番の近道 かなと思います。「位置ベロ本」の方は2024年8月発売であり、2023年にこの書籍があったら…と当時の私を振り返って思ってしまいます。

TSURUHASHI では Google Maps Platform を使っていたのですが、その中で Google Maps Platform では対応しづらい要件を経験、地図技術に長けた外部よりいらした複数の方々より MapboxMapbox GL JS の存在を教えて頂きました。愛用している Mini Tokyo 3D でも使われている Mapbox GL JS であり、導入に迷いはありませんでした(なお、Mini Tokyo 3D の作者である草薙さん @nagix は Mapbox Japan のアンバサダーです)。

2024年夏からは Mapbox GL JS を使った地図の開発導入を開始。正直なところ、それまでフロントエンド技術は「みようみまね」でやり過ごしていたのですが、アプリケーションへのゼロからの地図 = Mapbox GL JS 導入ということで、React や各種 UI ライブラリとも真剣に向き合うことに。最初は何もかもわからない状態でしたが、今では Mapbox GL JS の勘所もある程度わかるようになり、React もある程度人並み程度にはわかるようになりました。

学習曲線がなかなか上向かない私は、プライベートの時間で一人勉強していたら何ヶ月も成果が出なかったと思います。 地図という副作用の塊同然の要素を React と一緒に取り扱うことで、React の様々な構文の活用に迫られた ことも良い経験でした。

2024年になってから、自分が携わっていない社内プロジェクトでも GIS や地図技術が重要技術として取り組まれ、社内でも GIS 仲間と呼べる人が増えています。1年と少し前、2023年を思い返すと相談できる人も少なかったのですが、特に2024年の後半は相対的に組織の規模が大きくなったことで嬉しい環境になっています。

社内では Google Maps や Mapbox GL JS 以外にも、Leaflet や deck.gl の活用事例もあり、横の連携を強化することで次の技術選定を良いものにしていきたいです。また Mapbox GL JS が多くのプロジェクトで活用されていることから、その「親戚」である Maplibre GL JS にも取り組んでみたいと考えています。

2024年、GIS のイベントにも多数参加した

2023年、社内で GIS について気軽に話せる仲間が少なかったことを解消する意味で、社外で行われる GIS 系イベントにも各種参加しました。

2023年夏の入社以降、2024年末までに参加した GIS イベントをまとめてみました。

2023年当初はオンラインイベントが多く、GIS の知人を増やす目標を達成しづらい状況でしたが、コロナ禍の気配がだいぶ無くなった2024年は現地参加イベントも増え、色々な方と対面で交流できたことが嬉しいです。それでもまだまだ GIS 界隈に知人が多いとは言えず、ぼっちになりがちではあります。とはいえ、もともと社交的な人間ではないこと、社会人なりたての2000年代にも(大規模イベントに参加して終始ぼっちで帰宅する)同じような思いをしたなぁと思い出しながら、今後も様々なイベントに足を運んで交流の機会を増やそうと思っています。

前述でも「一人で勉強していても一向に上向かない」といったことを書きましたが、長く社会人をやっていて、自分ひとりでは何もできないということは日々痛感します。会社に足を運び、同僚との日々の何気ない会話から得られる気づきというのは、少なくとも私にとって大きいもの。その何気ない日常が崩れた2020年のコロナ禍によるワークスタイルの激変は、(フルリモートによって)通勤の苦痛を取り去ったというポジティブな側面とともに、同時に私から偶発的な雑談とそこから得られる様々なものを奪いました。

トグルホールディングスでは、「週2〜3回は来てくださいね」というハイブリッド勤務が採用されており、雑談大好きな私は毎日出社しつつ、「〇〇さん、今日はリモートワークだけど、明日は来そうだから明日マップタイルサーバについて聞いてみよう」といったことが出来るのは大きな魅力です。

勤務体制と福利厚生 | toggle holdings Engineering Handbook の「リモートワークについての考え方」も参照下さい)

ここ10年ほど、複数のコミュニティイベントや過去の在職企業での社外向けエンジニアイベントにて、イベント運営側に回ることは毎月ペースであったのですが、参加者側としてこれほど頻繁にイベントに参加することはそれこそ10年ぶりくらいです。それも、興味先行の GIS の知識、そして先駆者の生の声を聞きたくて界隈で活躍する方々の交流を求めた結果なのかもしれません。

トグルホールディングスでも GIS の社外向けイベントをやってみよう!

GIS のイベントにたくさん参加した…という話をしましたが、もともとイベント運営に多数携わった身として、開催する側にも立ってみたいと考えるようになりました。様々な層(難易度、ジャンル、適用業種等)への GIS イベントへ参加しましたが、まだ埋められていない層は多いと感じています。であれば「やってみよう」の精神を発揮するしかないと。

そんな思惑もありつつ、来年2025年、トグルホールディングスでは自社主催の社外向けエンジニアイベントを定期的に開催しようと考えています。であれば GIS イベントもその枠組でできれば!ということで、 トグルホールディングス主催の GIS イベントを 2025年2月26日(水曜日)の夜に開催予定です 🎉

まだ計画中の段階ではありますが、2025年1月上旬にはトグルホールディングスの connpass グループで公開できればと考えています。興味ある方は是非 connpass グループのトップページにある「メンバーになる」ボタンを押して下さい!(イベント公開時に通知が飛びます)

トグルホールディングスが不動産業務に取り組む中で得た GIS や地図技術にまつわる各種知見を発表する場となるよう、これから準備を進めていきたいと思います。その他、(可能であれば)毎月ペースで、トグルホールディングスの各エンジニアが取り組んでいる各技術ジャンルの勉強会も上記 connpass グループで公開および開催していければと考えています。ご期待下さい!そしてご参加お待ちしています!

インポートの変更でvite devが数分から数秒になった

こんにちは。トグルホールディングス、AIエンジニアのマーカスです。 トグルホールディングスエンジニアアドベントカレンダーの23日目の記事です!

概要

- import mapboxgl from 'mapbox-gl'
+ import mapboxgl from '../lib/mapboxgl'

っていう謎の変更でvite devのビルド時間が2分から18秒までに短縮したについての話です。

背景

開発する途中で動作確認したいのはよくある例で、そこでJavaScriptツールでHMRという機能で変更点をすぐ見えるようになったはずなのに、何故か2分かかったのは謎です。また、vite buildを試したとき15秒しかかからないのはもっと謎です。

そこでgit bisectで悪いコミットを探し、インポートの変更を見つけました。

理由を知らないまま進むと今後同じことを起きるかもしれないため、徹底的に調査しました。

推測

TypeScriptの型検査でビルド時間かかるのはよくある話。

そこでmapbox-glのインポートを一つのファイル(../lib/mapboxgl.ts)にまとめて、型検査を1回でしか実行しないようにした、のが推測です。それを再現するために小さなプロジェクトを建ててみました。

(※ TypeScriptとは関係ないのビルドツール(rollupなど)が遅いの可能性もあるが、一旦ここで放っておきます)

// ../lib/mapboxgl.ts
import mapboxgl from 'mapbox-gl'

// mapboxglの処理

export default mapboxgl

再現

プロジェクトを建てる

発見したプロジェクトがHonoXを使ってるから同じものにしました。

pnpm create hono

# ? Target directory my-app
# ? Which template do you want to use? x-basic
# ? Do you want to install project dependencies? yes
# ? Which package manager do you want to use? pnpm

ディレクトリの構造

my-app
├── app
|  ├── client.ts
|  ├── global.d.ts
|  ├── islands
|  |  └── counter.tsx
|  ├── routes
|  |  ├── _404.tsx
|  |  ├── _error.tsx
|  |  ├── _renderer.tsx
|  |  └── index.tsx
|  └── server.ts
├── package.json
├── pnpm-lock.yaml
├── public
|  └── favicon.ico
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml

再現するファイルを作る

pnpm add mapbox-gl
// app/routes/index.tsx
import { createRoute } from 'honox/factory'
import Slow from '../islands/Slow'

export default createRoute((c) => {
  return c.render(<Slow />)
})
// app/islands/Slow.tsx
import mapboxgl from 'mapbox-gl'

export default function Slow() {
  const dummy = typeof mapboxgl
  return (
    <div>
      {dummy}
    </div>
  )
}
my-app
├── app
|  ├── client.ts
|  ├── global.d.ts
|  ├── islands
|  |  └── Slow.tsx
|  ├── routes
|  |  ├── _404.tsx
|  |  ├── _error.tsx
|  |  ├── _renderer.tsx
|  |  └── index.tsx
|  └── server.ts
├── package.json
├── pnpm-lock.yaml
├── public
|  └── favicon.ico
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml

よし!プロジェクトを建てられたので早速実験を実行する。

初期ビルド速度を試す

pnpm vite dev
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173
# 3.998502 seconds

わざと遅くさせる

先ほどちょっと言ったが、mapbox-glを何倍インポートしてたら遅くなるはず

graph TD;
    mapbox-gl-->A;
    mapbox-gl-->B;
    mapbox-gl-->C;
    A-->routes;
    B-->routes;
    C-->routes;
// app/islands/Slow.tsx
import mapboxgl from 'mapbox-gl'
import mapboxgl2 from 'mapbox-gl'
import mapboxgl3 from 'mapbox-gl'
import mapboxgl4 from 'mapbox-gl'
import mapboxgl5 from 'mapbox-gl'
import mapboxgl6 from 'mapbox-gl'
import mapboxgl7 from 'mapbox-gl'
import mapboxgl8 from 'mapbox-gl'
import mapboxgl9 from 'mapbox-gl'
import mapboxgl10 from 'mapbox-gl'

export default function Slow() {
  const dummy = typeof mapboxgl ||
    typeof mapboxgl2 ||
    typeof mapboxgl3 ||
    typeof mapboxgl4 ||
    typeof mapboxgl5 ||
    typeof mapboxgl6 ||
    typeof mapboxgl7 ||
    typeof mapboxgl8 ||
    typeof mapboxgl9 ||
    typeof mapboxgl10
  return (
    <div>
      {dummy}
    </div>
  )
}
pnpm vite dev # キャッシュを使わないように再起動(速度を測る前に実行するが今後略する)
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173
# 30.474034 seconds

よし!遅くなった。

修正

仮説から

ここで前の仮説からやってみよう。

// app/islands/Slow.tsx
import mapboxgl from '../lib/mapboxgl'
import mapboxgl2 from '../lib/mapboxgl'
import mapboxgl3 from '../lib/mapboxgl'
import mapboxgl4 from '../lib/mapboxgl'
import mapboxgl5 from '../lib/mapboxgl'
import mapboxgl6 from '../lib/mapboxgl'
import mapboxgl7 from '../lib/mapboxgl'
import mapboxgl8 from '../lib/mapboxgl'
import mapboxgl9 from '../lib/mapboxgl'
import mapboxgl10 from '../lib/mapboxgl'

export default function Slow() {
  const dummy = typeof mapboxgl ||
    typeof mapboxgl2 ||
    typeof mapboxgl3 ||
    typeof mapboxgl4 ||
    typeof mapboxgl5 ||
    typeof mapboxgl6 ||
    typeof mapboxgl7 ||
    typeof mapboxgl8 ||
    typeof mapboxgl9 ||
    typeof mapboxgl10
  return (
    <div>
      {dummy}
    </div>
  )
}
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173
# 17.119245 seconds

あれ?ちょっと早くなったがインポート1回みたいの速度じゃないな・・・ ../lib/mapboxglを1回インポートしてやってみよう

// app/islands/Slow.tsx
import mapboxgl from '../lib/mapboxgl'

export default function Slow() {
  const dummy = typeof mapboxgl
  return (
    <div>
      {dummy}
    </div>
  )
}
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173
# 4.015881 seconds

うん。mapbox-glがまだ数回読まれてる。

前のディレクトリ構造から

色んな試行錯誤してroutesのディレクトリ構造が原因なのは分かったんでここで試してみる。

作り直し

// app/routes/foo/index.tsx
import { createRoute } from 'honox/factory'
import Slow from '../../islands/Slow'

export default createRoute((c) => {
  return c.render(<Slow />)
})
// app/islands/Slow.tsx
import mapboxgl from 'mapbox-gl'

export default function Slow() {
  const dummy = typeof mapboxgl
  return (
    <div>
      {dummy}
    </div>
  )
}
my-app
├── app
|  ├── client.ts
|  ├── global.d.ts
|  ├── islands
|  |  └── Slow.tsx
|  ├── lib
|  |  └── mapboxgl.ts
|  ├── routes
|  |  ├── _404.tsx
|  |  ├── _error.tsx
|  |  ├── _renderer.tsx
|  |  └── foo
|  |     └── index.tsx
|  └── server.ts
├── package.json
├── pnpm-lock.yaml
├── public
|  └── favicon.ico
├── tsconfig.json
├── vite.config.ts
└── wrangler.toml
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173/foo
# 3.860002 seconds

よし!また4秒

遅くさせる(2回目)

// app/islands/Slow.tsx
import mapboxgl from 'mapbox-gl'
import mapboxgl2 from 'mapbox-gl'
import mapboxgl3 from 'mapbox-gl'
import mapboxgl4 from 'mapbox-gl'
import mapboxgl5 from 'mapbox-gl'
import mapboxgl6 from 'mapbox-gl'
import mapboxgl7 from 'mapbox-gl'
import mapboxgl8 from 'mapbox-gl'
import mapboxgl9 from 'mapbox-gl'
import mapboxgl10 from 'mapbox-gl'

export default function Slow() {
  const dummy = typeof mapboxgl ||
    typeof mapboxgl2 ||
    typeof mapboxgl3 ||
    typeof mapboxgl4 ||
    typeof mapboxgl5 ||
    typeof mapboxgl6 ||
    typeof mapboxgl7 ||
    typeof mapboxgl8 ||
    typeof mapboxgl9 ||
    typeof mapboxgl10
  return (
    <div>
      {dummy}
    </div>
  )
}
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173/foo
# 29.796043 seconds

また30秒

修正

// app/islands/Slow.tsx
import mapboxgl from '../lib/mapboxgl'
import mapboxgl2 from '../lib/mapboxgl'
import mapboxgl3 from '../lib/mapboxgl'
import mapboxgl4 from '../lib/mapboxgl'
import mapboxgl5 from '../lib/mapboxgl'
import mapboxgl6 from '../lib/mapboxgl'
import mapboxgl7 from '../lib/mapboxgl'
import mapboxgl8 from '../lib/mapboxgl'
import mapboxgl9 from '../lib/mapboxgl'
import mapboxgl10 from '../lib/mapboxgl'

export default function Slow() {
  const dummy = typeof mapboxgl ||
    typeof mapboxgl2 ||
    typeof mapboxgl3 ||
    typeof mapboxgl4 ||
    typeof mapboxgl5 ||
    typeof mapboxgl6 ||
    typeof mapboxgl7 ||
    typeof mapboxgl8 ||
    typeof mapboxgl9 ||
    typeof mapboxgl10
  return (
    <div>
      {dummy}
    </div>
  )
}
curl -o /dev/null -w "%{time_total} seconds\n" -s http://localhost:5173/foo
# 2.516782 seconds

4秒すらかからなかった!

余談

import mapboxgl from '../lib/mapboxgl'とは別

  • 絶対パスimport mapboxgl from '/home/my-app/app/lib/mapboxgl'
  • Viteのresolve.aliasimport mapboxgl from '@/lib/mapboxgl'

をやる時もimport mapboxgl from 'mapbox-gl'みたいな速度、数回読まれてる。

分かったこと

  • HonoXが使ってるviteのバージョンか設定がバグっている
  • ファイルのまとめ方によってビルド時間が変わる
  • インポートのやり方によってビルド時間が変わる

今度ビルドが遅くなった時インポートのやり方を変えるべきかもしれないです。 また、viteのバグか、HonoXのみの問題かはまた別の記事に記載したいと思います!

Reactデザインパターン:Compound Component

こんにちは。トグルホールディングスプロダクトユニットのラファエルです。 トグルホールディングスエンジニアアドベントカレンダーの22日目の記事です!

はじめに

React デザインパターンは、React 開発におけるよくある問題に対する、実績のある解決策です。これらは、問題を効率的に解決しつつ、コードをきれいで保守しやすい状態に保つのに役立ちます。React が進化するにつれて、新しい課題に対応し、より良いアプリケーションを構築するために、新しいデザインパターンが登場しています。

今回は、開発者が直面しがちな共通の課題、大きなコンポーネントの扱い方についてお話しします。

課題

例えば、ユーザーフォーム のようなコンポーネントを構築しているとします。ユーザーは、名前メールアドレスといった基本的な情報を入力できます。

最初はサービスが立ち上げられたばかりなので、必要な情報はそれほど多くありません。シンプルなフォームで十分です。

しかし、サービスが成長し、より多くのユーザーを獲得すると、性別年齢といった、より詳細な情報を収集することで製品を改善したいと考えるようになります。

こうして、フォームコンポーネントは新しい要件を満たすために拡張されていきます。

さらにサービスが大成功を収めると、ユーザーへの感謝の印としてギフトを贈りたいと考えるかもしれません🎉。そのためには、ユーザーの住所を収集する必要があります。

import React from "react";
import { useForm, SubmitHandler } from "react-hook-form";

// フォームデータの型を定義
type FormData = {
  name: string;
  email: string;
  gender: string;
  age: number;
  address: string;
  postalCode: string;
};

const UserForm: React.FC = () => {
  const {
    register, // フォームフィールドを登録
    handleSubmit, // フォームの送信を処理
    formState: { errors }, // バリデーションエラーの状態を管理
  } = useForm<FormData>();

  // フォーム送信ハンドラー
  const onSubmit: SubmitHandler<FormData> = (data) => {
    console.log("Form Data:", data);
    alert("フォームが正常に送信されました!");
  };

  return (
    <div style={{ maxWidth: "400px", margin: "0 auto", padding: "1rem" }}>
      <h2>ユーザーフォーム</h2>
      <form onSubmit={handleSubmit(onSubmit)}>
        {/* 名前フィールド */}
        <div style={{ marginBottom: "1rem" }}>
          <label>名前:</label>
          <input
            {...register("name", { required: "名前は必須項目です" })}
            placeholder="名前を入力してください"
            style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
          />
          {errors.name && (
            <span style={{ color: "red", fontSize: "0.875rem" }}>
              {errors.name.message}
            </span>
          )}
        </div>

        {/* メールフィールド */}
        <div style={{ marginBottom: "1rem" }}>
          <label>メールアドレス:</label>
          <input
            {...register("email", {
              required: "メールアドレスは必須項目です",
              pattern: {
                value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
                message: "有効なメールアドレスを入力してください",
              },
            })}
            placeholder="メールアドレスを入力してください"
            style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
          />
          {errors.email && (
            <span style={{ color: "red", fontSize: "0.875rem" }}>
              {errors.email.message}
            </span>
          )}
        </div>

        {/* 性別フィールド */}
        <div style={{ marginBottom: "1rem" }}>
          <label>性別:</label>
          <select
            {...register("gender", { required: "性別を選択してください" })}
            style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
          >
            <option value="">性別を選択</option>
            <option value="male">男性</option>
            <option value="female">女性</option>
            <option value="other">その他</option>
          </select>
          {errors.gender && (
            <span style={{ color: "red", fontSize: "0.875rem" }}>
              {errors.gender.message}
            </span>
          )}
        </div>

        {/* 年齢フィールド */}
        <div style={{ marginBottom: "1rem" }}>
          <label>年齢:</label>
          <input
            type="number"
            {...register("age", {
              required: "年齢は必須項目です",
              min: { value: 1, message: "1歳以上を入力してください" },
              max: { value: 120, message: "120歳以下を入力してください" },
            })}
            placeholder="年齢を入力してください"
            style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
          />
          {errors.age && (
            <span style={{ color: "red", fontSize: "0.875rem" }}>
              {errors.age.message}
            </span>
          )}
        </div>

        {/* 住所フィールド */}
        <div style={{ marginBottom: "1rem" }}>
          <label>住所:</label>
          <textarea
            {...register("address", {
              required: "住所は必須項目です",
              minLength: { value: 10, message: "住所は10文字以上で入力してください" },
            })}
            placeholder="住所を入力してください"
            style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem", height: "80px" }}
          />
          {errors.address && (
            <span style={{ color: "red", fontSize: "0.875rem" }}>
              {errors.address.message}
            </span>
          )}
        </div>

        {/* 郵便番号フィールド */}
        <div style={{ marginBottom: "1rem" }}>
          <label>郵便番号:</label>
          <input
            type="text"
            {...register("postalCode", {
              required: "郵便番号は必須項目です",
              pattern: {
                value: /^[0-9]{5,6}$/,
                message: "郵便番号は5桁または6桁で入力してください",
              },
            })}
            placeholder="郵便番号を入力してください"
            style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
          />
          {errors.postalCode && (
            <span style={{ color: "red", fontSize: "0.875rem" }}>
              {errors.postalCode.message}
            </span>
          )}
        </div>

        {/* 送信ボタン */}
        <button
          type="submit"
          style={{
            padding: "0.5rem 1rem",
            backgroundColor: "blue",
            color: "white",
            border: "none",
            cursor: "pointer",
          }}
        >
          送信
        </button>
      </form>
    </div>
  );
};

export default UserForm;

このような課題が生じる理由

新しい機能やフィールドをフォームに追加していくと、次のような問題が発生します:

  • サイズが大きくなる: ロジックやUI要素が増え、コンポーネントが肥大化します
  • 読みにくくなる: コードが煩雑になり、理解や変更が困難になります
  • 再利用が難しくなる: コンポーネントの一部を他の場所で再利用するのが難しくなります

解決策

このような複雑さを解消するための方法の1つが、Compound Component パターンを使用して、コンポーネントを小さく管理しやすいパーツに分割することです。

Compound Component パターンを使用することで、親コンポーネントと、それを補完する複数の子コンポーネントをシームレスに組み合わせることができます。この方法を使えば、コードがよりモジュール化され、保守性が向上します。

では、この大きなフォームをクリーンでモジュール化された Compound Component にリファクタリングしてみましょう! 🚀

状態を共有可能にする

この場合、formState はフォームの状態を管理しています。この状態とフォームメソッドを複数のコンポーネント間で共有するには、React Context APIを利用することができます。

こうすることで、フォーム全体の状態 (formState) と必要なメソッドを中央集約したUser Form Contextを作成できます。これにより、すべてのサブコンポーネントが親コンポーネントに依存せずに、フォームの状態を操作できるようになります。

実際に実装してみましょう!

// フォームデータ型を定義
export type UserFormData = {
  name: string;
  email: string;
  gender: string;
  age: number;
  address: string;
  postalCode: string;
};

// コンテキストの型を定義
type UserFormContextType = {
  formMethods: UseFormReturn<UserFormData>;
  onSubmit: SubmitHandler<UserFormData>;
};

// コンテキストを作成
const FormContext = createContext<UserFormContextType | null>(null);

// フォームコンテキストを使用するためのカスタムフック
export const useFormContext = () => {
  const context = useContext(FormContext);
  if (!context) {
    throw new Error("useFormContext は FormProvider 内でのみ使用できます");
  }
  return context;
};

// メインの UserForm コンポーネント
export const UserForm = ({ children }: { children: ReactNode }) => {
  const formMethods = useForm<UserFormData>();

  const onSubmit: SubmitHandler<UserFormData> = (data) => {
    console.log("フォームデータ:", data);
    alert("フォームが正常に送信されました!");
  };

  return (
    <FormContext.Provider value={{ formMethods, onSubmit }}>
      <div style={{ maxWidth: "400px", margin: "0 auto", padding: "1rem" }}>
        <h2>ユーザーフォーム</h2>
        <form onSubmit={formMethods.handleSubmit(onSubmit)}>{children}</form>
      </div>
    </FormContext.Provider>
  );
};

コンポーネントをサブコンポーネントに分割する

UserForm コンポーネントを拡張し、次の3つの汎用的なサブコンポーネントに分割します:

Basic: 名前やメールアドレスなどの基本的な情報を処理します。 Profile: 性別や年齢などのプロフィール情報を処理します。 Address: ユーザーの住所や郵便番号などの詳細情報を収集します。 この構造により、フォームがモジュール化され、管理が容易になります。

UserForm.Basic = () => {
  const {
    formMethods: { register, formState: { errors } },
  } = useFormContext();

  return (
    <>
      {/* 名前フィールド */}
      <div style={{ marginBottom: "1rem" }}>
        <label>名前:</label>
        <input
          {...register("name", { required: "名前は必須項目です" })}
          placeholder="名前を入力してください"
          style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
        />
        {errors.name && (
          <span style={{ color: "red", fontSize: "0.875rem" }}>
            {errors.name.message}
          </span>
        )}
      </div>

      {/* メールアドレスフィールド */}
      <div style={{ marginBottom: "1rem" }}>
        <label>メールアドレス:</label>
        <input
          {...register("email", {
            required: "メールアドレスは必須項目です",
            pattern: {
              value: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
              message: "有効なメールアドレスを入力してください",
            },
          })}
          placeholder="メールアドレスを入力してください"
          style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
        />
        {errors.email && (
          <span style={{ color: "red", fontSize: "0.875rem" }}>
            {errors.email.message}
          </span>
        )}
      </div>
    </>
  );
};

UserForm.Profile = () => {
  const {
    formMethods: { register, formState: { errors } },
  } = useFormContext();

  return (
    <>
      {/* 性別フィールド */}
      <div style={{ marginBottom: "1rem" }}>
        <label>性別:</label>
        <select
          {...register("gender", { required: "性別を選択してください" })}
          style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
        >
          <option value="">性別を選択</option>
          <option value="male">男性</option>
          <option value="female">女性</option>
          <option value="other">その他</option>
        </select>
        {errors.gender && (
          <span style={{ color: "red", fontSize: "0.875rem" }}>
            {errors.gender.message}
          </span>
        )}
      </div>

      {/* 年齢フィールド */}
      <div style={{ marginBottom: "1rem" }}>
        <label>年齢:</label>
        <input
          type="number"
          {...register("age", {
            required: "年齢は必須項目です",
            min: { value: 1, message: "1歳以上を入力してください" },
            max: { value: 120, message: "120歳以下を入力してください" },
          })}
          placeholder="年齢を入力してください"
          style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
        />
        {errors.age && (
          <span style={{ color: "red", fontSize: "0.875rem" }}>
            {errors.age.message}
          </span>
        )}
      </div>
    </>
  );
};

UserForm.Address = () => {
  const {
    formMethods: { register, formState: { errors } },
  } = useFormContext();

  return (
    <>
      {/* 住所フィールド */}
      <div style={{ marginBottom: "1rem" }}>
        <label>住所:</label>
        <textarea
          {...register("address", {
            required: "住所は必須項目です",
            minLength: { value: 10, message: "住所は10文字以上で入力してください" },
          })}
          placeholder="住所を入力してください"
          style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem", height: "80px" }}
        />
        {errors.address && (
          <span style={{ color: "red", fontSize: "0.875rem" }}>
            {errors.address.message}
          </span>
        )}
      </div>

      {/* 郵便番号フィールド */}
      <div style={{ marginBottom: "1rem" }}>
        <label>郵便番号:</label>
        <input
          {...register("postalCode", {
            required: "郵便番号は必須項目です",
            pattern: {
              value: /^[0-9]{5,6}$/,
              message: "郵便番号は5桁または6桁で入力してください",
            },
          })}
          placeholder="郵便番号を入力してください"
          style={{ width: "100%", padding: "0.5rem", marginTop: "0.25rem" }}
        />
        {errors.postalCode && (
          <span style={{ color: "red", fontSize: "0.875rem" }}>
            {errors.postalCode.message}
          </span>
        )}
      </div>
    </>
  );
};

使い方

UserForm とそのサブコンポーネントを実装すれば、以下のように簡単に使えるようになります:

import React from "react";
import UserForm from "./UserForm";

const App: React.FC = () => {
  return (
    <UserForm>
      <UserForm.Basic />
      <UserForm.Profile />
      <UserForm.Address />
    </UserForm>
  );
};

export default App;

この方法により、フォームの各部分が独立しつつ、React Context API を通じて状態とロジックが一元管理されます。

Compound Component パターンのメリット

  • モジュール化: サブコンポーネントは独立しており、コードが読みやすく保守しやすい
  • 再利用性: 個々のサブコンポーネントを、アプリケーションの他の部分で再利用可
  • スケーラビリティ: 新しいフィールドやセクションを簡単に追加可能
  • 状態の集中管理: React Context API を使用することで、すべての状態とメソッドをシームレスに共有

Compound Component パターンを使用することで、フォームをクリーンでモジュール化された状態に保ちつつ、React アプリケーションの複雑さを軽減することができます。次回のプロジェクトでぜひ試してみてください! 🚀

Cursor上級テクニック〜月額$20の真価を解放せよ〜

こんにちは、トグルホールディングスのAIエンジニアの中村です!

トグルホールディングスエンジニアアドベントカレンダーの21日目の記事です!

元記事は以下です。

zenn.dev

本記事では、Cursorを基本的に使いこなしている開発者がさらに一歩進んで活用するためのニッチなテクニックを紹介します。

「Cursor Proにせっかく月20ドル払っているから、もう一段活用の幅を広げたい!」 という方にこそ読んでいただきたい内容です!

はじめに

Cursorの主要機能を一通り使いこなしている方も多いかと思います。
しかし、月20ドルという投資をするからには、表面上の使い方だけではもったいない!

本記事では、案外見落とされがちな設定や機能を掘り下げて、Cursorの潜在能力をさらに引き出すための具体的なテクニックを紹介します。

少しの手間をかけるだけで、Cursorはあなたの プロジェクト全体を俯瞰してサポートする頼れる“参謀” になり得るのです!

www.cursor.com

1. ChatとComposer、それぞれの真価を知る

Cursorには大きく分けて「Chat」と「Composer」の2種類のモードが存在します。公式ドキュメントでは以下のように定義されています。

Cursor Chat helps you search and understand your code.
Use it to explore your codebase, ask questions, and get explanations. You can search your code with ⌘⏎.

Composer helps you write and edit code.
It provides a workspace where you can generate new code and apply changes directly to your files.

一言でまとめると、Chatはコードベースの探索・理解に特化し、Composerはコードの生成・編集に特化しています。

Chatモード

  • コードベースの探索と理解が主目的
    • コードに関する質問や説明を求める際に適しています。
    • ⌘⏎ でコードベース内検索ができるため、特定ファイルやメソッドの位置をサッと探して知りたい情報を得るのに便利です。
  • 素早い回答が必要な場合に最適
    • Chatウィンドウに気軽に問いかける感覚でやり取りができ、即時的な返信を得られます。
    • 要件定義やバグ発見時など、手早く「これってどうなってたっけ?」と確認したい時にも重宝します。

Composerモード

  • コードの生成と編集が主目的
    • ⌘I で起動して、⌘N で新規Composerを作成。
    • Composer上でコードを生成・編集し、その結果をファイルに直接適用することが可能です。
  • より構造的なコード提案や編集作業に向いている
    • 単発の質問というよりは、連続的・段階的にコードを作り上げたいときに最適です。
    • やや複雑なコードブロックの生成や既存ロジックのリファクタリングなど、腰を据えて作業したい場合におすすめです。

どう使い分ける?

要件定義フェーズやバグ調査にはChat

  • プロジェクトの仕様や既存コードの挙動を掴むときに役立ちます。
  • また、クイックに疑問を解消したい場合もChatでサクッと聞くのがおすすめです。

実装フェーズやリファクタリングにはComposer

  • 新規コードを書く・既存コードを大幅に書き換えるなど、手が大きく動く作業にはComposerがピッタリ。
  • 細かい修正から大きな機能追加まで、Composerなら生成したコードを即座にファイルに反映してくれます。

上記のように、 Chatは「探索と理解」・Composerは「生成と編集」 という役割が明確に分かれているため、プロジェクトの進行状況や目的に合わせて上手に切り替えましょう。最初の要件固めやコードリーディングにはChat、その後の実装と改修にはComposer、という流れを意識するだけで、Cursorの活用効率はぐっと高まります。

2. Composerには2つのモードがある?!

Composerは、コードを生成・編集する「場所」としての役割を担いますが、その内部でもさらにNormalモードAgentモードという2つのモードがあります。公式ドキュメントによると、それぞれ特徴が異なるため、活用方法を知っておくことでComposerの潜在能力をフルに引き出せます。

Normalモード

  • 基本的なコード探索と生成に特化
    • コードベースやドキュメントの検索、Web検索など、必要な情報を取り込みつつコードをサクッと生成できます。
    • @symbolコマンド(@codebase@Definitionsなど)を使ってコードの一部を引っ張ってくる、といった素早い参照も可能です。
  • 使い勝手は「スタンダードなAIコーディング補助ツール」
    • より人間の指示や意図を素直に汲み取ってコードを生成するので、複雑な自動化よりは確実性重視の補助を求めるときに向いています。

Agentモード(より高度な支援)

  • プロアクティブに作業をサポート
    • @Recommended による関連コンテキストの自動取得や、ターミナルコマンドの実行なども可能。
    • たとえば「依存関係をインストールして」と指示すると、ターミナルコマンドを生成・実行するところまで支援してくれます。(※ 設定からYolo Modeの有効化が必要)
  • ファイルの作成や修正、意味的なコード検索も強力
    • 「この処理を最適化して」とお願いすると、自動で候補を示したり、関連するファイルも開いて修正してくれたりします。
    • 内容理解を伴った提案を行うため、ある程度複雑なロジックでも一気に手伝ってもらえるのがAgentモードの強みです。

ペアプログラミング感覚のAgent活用事例

  • コードレビューの自動化
    • 「このファイルの可読性を改善して」「テストカバレッジを上げて」と依頼するだけで、Agentが直接ファイルを修正案ごと提示してくれます。
    • もちろん修正結果を丸呑みにするのではなく、提案を見ながら最終確認する形で進めると安心です。
  • 新規機能の叩き台を一気に生成
    • 「ユーザー管理機能を追加してほしい。エンドポイントは/〇〇、DBは××を使って」など、要件を伝えると雛形コードをサッと生成してくれます。
    • その後、必要に応じてNormalモードで細かな修正を加えていく、といった使い分けも便利です。

NormalかAgentか、どう選ぶ?

  • まずはNormalモード

    • シンプルなコード補完や既存コードのリファクタリング、ちょっとした修正が中心ならNormalで十分。
  • 強力に自動化したいならAgentモード

    • ターミナル操作や複数ファイルの横断的な修正、関連ファイルの検索・自動修正など、より手厚いサポートが欲しいならAgentを投入。

NormalモードとAgentモードを状況に合わせて使い分けることで、 「ちょっとした補助」から「がっつり手を動かしてもらう自動化」 までカバー可能になります。

3. Rules for AI と .cursorrulesでCursorの出力を思い通りにチューニングする

Cursorでより高品質・一貫性のあるコードを得るために、グローバル設定プロジェクト単位設定の2つのルール設定が重要です。これらを適切に活用することで、AIの提案内容を自分たちの開発方針に寄せることが可能になります。

「Rules for AI」でグローバルな振る舞いを定義する

  • 設定場所
    Cursor Settings > General > Rules for AI

  • 適用範囲
    Chat、Composer、Ctrl/⌘ + Kなど、Cursorが提供するすべての機能

  • 主な活用例

    1. 使用言語・実装スタイルの規定
      • 例:「TypeScriptで型を明示し、純粋関数を基本とした関数型プログラミングを採用。副作用は専用ユーティリティに隔離する」
      • これにより、Cursorは型安全で副作用の少ないコードを自然と提案するようになり、一貫したコーディングスタイルを維持できます。
    2. コーディング規約の共有
      • 変数の命名規則、フォーマット、ファイル分割方針など
    3. 基本フローの指定
      • 例:「最初に擬似コードを書き、それを確認してから本実装に移る」「誤りがあれば謝罪ではなく修正を優先」
      • AIに作業工程を意識させることで、初めからテストを含めたコード提案など、品質担保につながる可能性があります。

.cursorrulesでプロジェクト固有のルールを管理する

グローバル設定とは別に、プロジェクト単位でルールをカスタマイズできるのが.cursorrulesファイルです。

  • 配置場所
    プロジェクトのルートディレクトリ(例:/my-project/.cursorrules

  • 特徴

    1. プロジェクト固有の要件やディレクトリ構造
      • 「Reactコンポーネントsrc/components/下に」「DBアクセスには必ず共通モジュールを使用」など、ローカルなルールを厳密に反映。
    2. チームの独自方針を反映
      • 「コメントは英語で統一」など、チームや製品の方針を強制できる。
    3. グローバル設定との使い分け
      • 全体的なコーディング規約は「Rules for AI」に、プロジェクト固有ルールは.cursorrulesに、と使い分けるとスマートです。

これらのルール設定を駆使することで、Cursorはよりプロジェクトの意図に合った返答やコード生成をしてくれるようになります。

ぜひ「Rules for AI」と.cursorrulesを使い分けて、チームや個人のコーディングスタイルに最適化された開発環境を整えてみてください。

4. .cursorignoreで参照情報を厳選し、的確な回答を得る

大規模なリポジトリほど、Cursorが参照する必要のないソースや生成ファイルが含まれています。そうしたノイズをカットし、 「必要な情報だけ」 をCursorに読み込ませる仕組みとして有効なのが、.cursorignoreファイルです。

インデックス対象から外す戦略

Cursorはプロジェクトを自動でインデックスし、質問への回答やコード補完に活用します。しかし、膨大なファイルが含まれるほど検索コストが上がり、回答の正確性や速度が下がりがち。そこで.cursorignoreを使い、不要ファイルやフォルダを指定してインデックス対象を絞り込むことで、処理を効率化し、回答の精度を高めることができます。

  • スピードアップ
    不要ファイルが除外されることで、Cursorが処理するデータ量が減り、応答が早くなる。

  • 精度向上
    余計なビルド成果物やログを排除して、本当に参照すべきコードだけをAIが読み取りやすくなる。

Tip: .cursorignore.gitignoreと同じ要領でパターンマッチを行うため、既存の.gitignoreを流用したり、そこに追記する形が最も簡単です。

.cursorignoreの例

# ビルド生成物、環境を除外
build/
venv/
node_modules/

# ログファイル・キャッシュファイルを一括除外
*.log
__pycache__/

# 特定の設定ファイルを除外
config.json

このように、プロジェクト固有の生成物やキャッシュディレクトリを積極的に除外することで、Cursorは “最新かつ必要な情報” にだけフォーカスして回答を返すようになります。コード生成に関連のないファイル群によりインデックスが乱されないため、クリーンなコードベース を維持しやすくなるのが大きなメリットです。

モノレポ構成との相性

1つのリポジトリ内に複数のプロジェクトを抱えるモノレポ構成では、開発者が取り組んでいる一部プロジェクト以外はインデックス対象に含めないほうが効率的な場合があります。たとえば、リポジトリ全体をインデックスする必要がなく、一部のディレクトリだけ参照すれば十分ということも多いでしょう。

  • メリット

    • 他プロジェクトに関する不要なコードの解析を避けられ、回答の精度とスピードが向上
    • 開発中のフォルダだけを集中して読ませるため、ノイズが大幅に減少
  • 運用方法

    • .cursorignoreに、担当外プロジェクトのディレクトリを明示的に除外する
    • 開発フェーズや担当範囲が変わったら、適宜 .cursorignore を更新する

こうすることで、モノレポ全体を参照対象とする無駄を回避し、必要な範囲だけCursorに食わせる柔軟な運用が可能になります。

5. Notepadsを活用して知識ベースを構築し、AIと連携する

Notepadsは、Markdown形式でプロジェクト情報やテンプレート、コードスニペットなどを一元管理できる機能です。表面的には「個人Wiki」に見えますが、その真価はChatやComposerから@Notepadを使って呼び出せるところにあります。これによって、メモやコード例、ドキュメントテンプレートをいつでも即座にプロンプトへ組み込み、AIに参照させることが可能です。以下に、活用のポイントをまとめます。

  1. ドキュメント・テンプレートの集中管理

    • アーキテクチャの検討メモや要件定義のテンプレート、特定のフレームワーク設定をMarkdown形式で整理。
    • それを@Notepad経由でChatやComposerに展開すれば、必要なタイミングでスムーズに内容を呼び出せます。
    • たとえば「AIに投げる標準プロンプト(TypeScriptでのコード生成指示)」「エッジケースのテスト用テンプレート」などを準備しておくと、プロンプト記述の時間を削減できます。
  2. コードスニペット&知識ベースをAIが即座に参照

    • よく使う関数やクラスの実装例、トラブルシューティング方法をNotepadsに保存しておけば、AIがその情報をもとに回答を行いやすくなります。
    • たとえば新機能のコードを書いている最中に「以前作った○○のサンプルを参考にしたい」と思ったら、@Notepadで該当するノートを呼び出してAIに統合的に提示し、最適な実装案を受け取ることが可能です。
  3. AIとの相乗効果でメモを強化

    • Notepads内のMarkdownに対し、「コードブロックを整理して」「要点をリスト化して」といった依頼をAIへ出し、プロンプトから直接リライトや要約を行うとドキュメント保守がスムーズに。
    • ドキュメント生成機能などを組み合わせることで、実装と資料がリアルタイムにリンクし、情報が更新しやすい仕組みを作れます。

このように、Notepadsを使えば単なる個人Wikiに留まらず、プロジェクト情報やコード例を即座にAIへ“注入”できる柔軟な知識ベースとして運用できます。プロンプトを使い回す頻度が高い開発者や、チーム内で共通テンプレートを再利用したいシーンでは、特に効果が大きいでしょう。

6. まとめ:Cursorにさらなる価値を見出すために

ここまで紹介してきたChat / Composerの使い分けやAgentモード、Rules for AI / .cursorrulesによる高度なチューニング、.cursorignoreやNotepadsなどの隠れた機能は、いずれも日々の開発ワークフローを大きく変えるポテンシャルを持っています。

  • ChatやComposerを状況に応じて使い分けることで、コードリーディングから実装・リファクタリングまでスムーズに進行
  • Agentモードを導入すれば、複雑な自動化やファイル横断的な修正もラクラク
  • Rules for AIや.cursorrulesで出力をコントロールし、チームやプロジェクトの開発方針に沿った一貫性のあるコードを生成
  • .cursorignoreを駆使してモノレポや大規模プロジェクトのノイズを排除し、必要な情報だけを効率よく読み込む
  • Notepadsで知識ベースを構築して必要なテンプレートやスニペットをいつでも@Notepadで呼び出せるようにする

はじめは設定やルールづくりが面倒に感じるかもしれませんが、一度整備すれば毎日の開発効率やコードの品質が明らかに向上します。Cursorを単なる「自動コード生成ツール」としてだけでなく、 プロジェクト全体を俯瞰してサポートする頼れる“参謀” として使いこなしていきましょう。

Cursorの機能を最大限に生かして、ぜひあなたの開発現場に合わせた最適な使い方を模索してみてください!

データの前処理はDuckDBで ー位置情報データも取り扱うー

トグルホールディングスの政岡です。トグルアドベントカレンダー21日目の記事です。元記事はこちらです。

zenn.dev

概要

DuckDBPostGISのような空間関数を使用してデータの前処理をしてみます。

今回は、駅データ.jpの駅名データを使用して、位置情報を含むcsvファイルを取り扱います。複数路線が存在する駅は同じ駅名で複数地点登録されているため、これを整理します。路線情報を取り除き、単一の駅名のみを抽出することが目的です。具体的には、JR山手線の東京駅と東京メトロ銀座線の東京駅が一つの「東京駅」の代表点で一意となるようにデータを整理します。操作としては、複数地点の緯度経度座標の重心をとって、重複する駅名を取り除きます。

バージョンとディレクトリ構成

# version
duckdb v1.1.3

データは./dataディレクトリに配置します。

データインポート

今回は駅データ.jpの駅名データを使用します。

csvファイルからデータを取り込んで、列名一覧を取得してみます。 show tbl;で列名の一覧を表示できます。

show select * from './data/stations.csv';
┌────────────────┬─────────────┬─────────┬─────────┬─────────┬─────────┐
│  column_name   │ column_type │  null   │   key   │ default │  extra  │
│    varchar     │   varchar   │ varchar │ varchar │ varchar │ varchar │
├────────────────┼─────────────┼─────────┼─────────┼─────────┼─────────┤
│ station_cd     │ BIGINT      │ YES     │         │         │         │
│ station_g_cd   │ BIGINT      │ YES     │         │         │         │
│ station_name   │ VARCHAR     │ YES     │         │         │         │
│ station_name_k │ VARCHAR     │ YES     │         │         │         │
│ station_name_r │ VARCHAR     │ YES     │         │         │         │
│ line_cd        │ BIGINT      │ YES     │         │         │         │
│ pref_cd        │ BIGINT      │ YES     │         │         │         │
│ post           │ VARCHAR     │ YES     │         │         │         │
│ address        │ VARCHAR     │ YES     │         │         │         │
│ lon            │ DOUBLE      │ YES     │         │         │         │
│ lat            │ DOUBLE      │ YES     │         │         │         │
│ open_ymd       │ VARCHAR     │ YES     │         │         │         │
│ close_ymd      │ VARCHAR     │ YES     │         │         │         │
│ e_status       │ BIGINT      │ YES     │         │         │         │
│ e_sort         │ BIGINT      │ YES     │         │         │         │
├────────────────┴─────────────┴─────────┴─────────┴─────────┴─────────┤
│ 15 rows                                                    6 columns │
└──────────────────────────────────────────────────────────────────────┘

今回の目的は、路線情報を取り除き、単一の駅名のみを抽出することなので、これに必要な列を取得します。station_g_cdは駅のグループIDです。後で使用するので取り込んでおきます。

SELECT
        station_g_cd   -- 駅グループID
        ,station_name   -- 駅名
        ,address        -- 駅の住所
        ,lon            -- 駅の軽度
        ,lat            --  駅の緯度
FROM
        './data/stations.csv'
LIMIT 3;

┌──────────────┬──────────────┬─────────────────────────────────┬────────────┬───────────┐
│ station_g_cd │ station_name │             address             │    lon     │    lat    │
│    int64     │   varcharvarchar             │   double   │  double   │
├──────────────┼──────────────┼─────────────────────────────────┼────────────┼───────────┤
│      1110101 │ 函館         │ 北海道函館市若松町12-13     │ 140.72641341.773709 │
│      1110102 │ 五稜郭       │ 函館市亀田本町                  │ 140.73353941.803557 │
│      1110103 │ 桔梗         │ 北海道函館市桔梗3丁目41-36 │ 140.72295241.846457 │
└──────────────┴──────────────┴─────────────────────────────────┴────────────┴───────────┘

よく見るデータベースの形式になりました。

地理空間情報を取り扱う

GEOMETRY型へ変換

住所のすべて位置がlon, lat列に分離していて取り扱いづらいので、GEOMETRY型にしてみましょう。 GEOMETRY型を取り扱うには、spatial拡張が必要なのでインストールとロードもしておきます。

INSTALL spatial;
LOAD spatial;

-- GEOMETRY型へ変換
SELECT ST_Point(lon, lat) AS geom FROM './data/stations.csv' LIMIT 3;
┌──────────────────────────────┐
│             geom             │
│           geometry           │
├──────────────────────────────┤
│ POINT (140.726413 41.773709) │
│ POINT (140.733539 41.803557) │
│ POINT (140.722952 41.846457) │
└──────────────────────────────┘

無事GEOMETRY型になりました。 GEOMETRY型はGeoJSONに対応しています。lon, latのような列構造になっていると、多角形などを表現するのが非常に大変なので、GEOMETRY型にしています。

中間テーブルの生成

ここでstationテーブルにデータを格納しておきます。 CREATE TABLE [table name]で新しいテーブルを作成できます。

CREATE OR REPLACE TABLE station AS
SELECT
    station_g_cd    -- 駅グループID
    ,station_name   -- 駅名
    ,address        -- 駅の住所
    ,ST_Point(lon, lat) AS geom
FROM
    './data/stations.csv';

地理空間関数を使用する

まずは、駅名が重複していることを確認します。試しに駅名の重複が11個以上のものを集めてみます。

SELECT
    t.num
    ,station_name
FROM (
    SELECT
        count(station_name) as num
        ,station_name
    FROM
        station
    GROUP BY
        station_name
    ) t
WHERE
    t.num >= 11;                                                                                    
┌───────┬──────────────┐
│  num  │ station_name │
│ int64 │   varchar    │
├───────┼──────────────┤
│    11 │ 横浜         │
│    11 │ 渋谷         │
│    11 │ 県庁前       │
│    16 │ 市役所前     │
│    12 │ 東京         │
│    13 │ 新宿         │
└───────┴──────────────┘

東京駅に集中してみます。

SELECT
        station_g_cd
        ,station_name
        ,address
        ,geom
FROM
        station
WHERE regexp_matches(station_name, '^東京$');

┌──────────────┬──────────────┬───────────────────────────────┬──────────────────────────────┐
│ station_g_cd │ station_name │            address            │             geom             │
│    int64     │   varcharvarchar            │           geometry           │
├──────────────┼──────────────┼───────────────────────────────┼──────────────────────────────┤
│      1130101 │ 東京         │ 東京都千代田区丸の内一丁目    │ POINT (139.766103 35.681391) │
│      1130101 │ 東京         │ 東京都千代田区丸の内一丁目9-1 │ POINT (139.766103 35.681391) │
│      1130101 │ 東京         │ 東京都千代田区丸の内一丁目    │ POINT (139.766103 35.681391) │
│      1130101 │ 東京         │ 東京都千代田区丸の内一丁目    │ POINT (139.766103 35.681391) │
...

station_g_cdは同一の駅に与えられるIDなので、この列でグループ化して緯度経度の重心をとってみます。 複数の点が一つの点に集約されました。

SELECT
    ST_Centroid(ST_Collect(ARRAY_AGG(geom))) AS centroid
FROM
    station
WHERE
    regexp_matches(station_name, '^東京$');

┌───────────────────────────────────────────────┐
│                   centroid                    │
│                   geometry                    │
├───────────────────────────────────────────────┤
│ POINT (139.76598674999994 35.681421166666674) │
└───────────────────────────────────────────────┘

上記の操作をすべてまとめてすべてのデータに適用すると以下のようになります。

CREATE OR REPLACE TABLE station_centroid AS
SELECT
    station_g_cd
    ,station_name
    ,ST_Centroid(
        ST_Collect(ARRAY_AGG(
            ST_Point(lon, lat)
            )
        )
    ) AS geom
FROM
    './data/stations.csv'
GROUP BY
    station_g_cd, station_name;

東京駅で確認してみると、緯度経度の重心で集約されているのがわかります。

 SELECT * FROM station_centroid
  WHERE regexp_matches(station_name, '^東京$');
┌──────────────┬──────────────┬───────────────────────────────────────────────┐
│ station_g_cd │ station_name │                     geom                      │
│    int64     │   varchar    │                   geometry                    │
├──────────────┼──────────────┼───────────────────────────────────────────────┤
│      1130101 │ 東京         │ POINT (139.76598674999994 35.681421166666674) │
└──────────────┴──────────────┴───────────────────────────────────────────────┘

データエクスポート

最後にこれをcsv形式で出力します。

COPY station_centroid TO './data/station_centroid.csv' (HEADER, DELIMITER ',');

GeoJSON形式で出力することも可能です。

COPY station_centroid TO './data/station_centroid.geojson'
WITH (FORMAT GDAL, DRIVER 'GeoJSON');

まとめ

DuckDBを使用することで簡単にSQLでデータの前処理を行うことができました。SQL使いたいけど、PostgreSQLの環境構築するのは面倒だなというときに是非活用してみてください。

WebStorm で愛用している機能たちを紹介したい

トグルホールディングスの鈴木(@suu_dev)です。トグルアドベントカレンダー19日目の記事です。

はじめに

エディタ、IDE は何を使いますか? React や Vue.js, TypeScript などを扱う方は、VSCode を利用することが多いのではないでしょうか。

WebStorm 自体、聞いたことがない方もいるかもしれません。

知らない、使ったことがない方には、是非知ってほしい WebStorm の素敵な機能たちを紹介します。

WebStorm とは

JetBrains 社製の IDE (統合開発環境) です。 あらかじめ色々な環境が用意されているのですぐにフロントエンド開発を始められます。

image.png

公式サイトの紹介欄

https://www.jetbrains.com/ja-jp/webstorm/features/

※ 最近まで有料だったのですが、非商用利用は無料になりました。

Git クライアント

個人的には JetBrains といえば、Git 機能をゲキ推ししたいです。 CLI での操作は一切必要なく、GUI だけで Git 操作が完結します。 視覚的に扱える機能が多いので、ミスが減り、効率もUP間違いなしです。

Github との連携

公式ドキュメントに詳しく書いてあるので割愛しますが、簡単に連携できます!

https://pleiades.io/help/webstorm/github.html

日常の操作

便利度: ★★★★★

git fetch 的な操作

ボタンを押すだけで fetch を実行します。 このあたりは VSCode と同じかもしれません。

image.png

git pull 的な操作

git pull もボタン一つで実行されます。

image.png

commit ログの表示

変更の差分を表示することができます。 必要なファイルだけチェックを入れて、 Commit することができます。

Mac: command + k Windows: ctrl + k

image.png

チェックボックスをONにするだけで、 amend commit もできるのが便利です。

image.png

push 作業

Mac: command + shift + k Windows: ctrl + shift + k

image.png

commit のリセット

image.png

cherry-pick とか

cherry-pick も GUI で一瞬で完了です。

image.png

merge or rebase

GUI ですぐにできます。

image.png

スカッシュなどもできます

image.png

このコミットいらないなー

そんなときはコミットの破棄も簡単に実行できます。

image.png

シェルブ機能

便利度: ★★★★★

正中の変更を退避することができます。 退避したファイルなどは、シェルブタブから確認することができます。

image.png

右クリック > アンシェルブ ですぐにファイルを作業時の状態に戻すことが出来ます。 image.png

Git stash に似ていますが、シェルブは IDE の保存領域を使うので、ローカルで完結します。 以下みたいなケースのときによく使います。

  • 機能開発中に、コードレビューの依頼が来て、一旦退避したいとき
  • 機能を作ってみたけど、しっくり来ないとき

便利なのは、一部だけを適用することができます。

image.png

差分画面から<<で一部だけを取り込む事もできます。

image.png

これ本当に便利です。 ローカルだけで気軽に変更を退避したり、戻したり出来るので、ヘビーユーズしています。

https://pleiades.io/help/webstorm/shelving-and-unshelving-changes.html

コンフリクト修正

便利度: ★★★★★

コンフリクト修正を GUI で簡単に行うことができます。 コンフリクトしている箇所は、赤。 コンフリクトしていない箇所は青で差分が表示されます。

左右で見比べながら、変更を修正できるので、コンフリクトしても焦ることなく作業できます。

image.png

ローカルとリモートのコンフリクトしていない青い差分は、下記のボタンを押下することで一気に取り込むことが出来ます。

image.png

この差分が0になればコンフリクト解消となります。

image.png

Code With Me

便利度: ★★★

コードシェアもできます。 VSCode Live Share と同じような機能です。

image.png

そして、この記事を執筆しながら知ったのですが、Code With Me はライブシェア内で通話もできるようです。

image.png

ただ私の周りは、みんな VSCode や Cursor を使っているので、Code With Meしたことがありません。やってみたいです。

https://pleiades.io/help/webstorm/code-with-me.html

AI Assistant

便利度: ★★★

Github Copilot のような機能です。有料機能です。 2024/12/19 現在は税込み1430円です。

わたしは、会社の経費で利用させていただいております。ありがとうございます!!

image.png

Chat形式で OpenAI-gpt-4oや、gemini-pro-1.5 などのモデルが利用可能です。

#を活用することで、より多くのコンテキストを与えることができます。

command 説明
#thisFile 現在開いているファイルを参照します。
#selection エディターで現在選択されているコードの一部を参照します。
#localChanges コミットされていない変更を指します。
#commit プロンプトにコミット参照を追加します。呼び出されたポップアップからコミットを選択するか、コミットハッシュを手動で書き込むことができます。
#file 現在のプロジェクトからファイルを選択できるポップアップを呼び出します。ポップアップから必要なファイルを選択するか、ファイル名 (例: #file:Foo.md) を入力します。
#symbol プロンプトにシンボルを追加します (例: #symbol:FieldName)。
#schema データベーススキーマを参照します。データベーススキーマをアタッチすると、スキーマのコンテキストで生成された SQL クエリの品質を向上させることができます。

予測入力機能もあります。 予測入力の精度は、肌感で Github Copilot のほうが良い気がしています。 image.png

選択項目に対して、リファクタリングの提案や、Docsの追加、ユニットテストの生成なども実施してくれます。 image.png

https://pleiades.io/help/webstorm/ai-assistant.html#install-ai-assistant-plugin

DB 操作

便利度: ★★★★

これは最近無料になりました! IntelliJ には標準搭載されていましたが、ついに WebStorm でも無料で使えるようになり嬉しい限りです。

IDE 上で直接 DB も操作できてしまうのです。

データソースの登録

ホスト名、ユーザー名、パスワードなどを入力し、Test Connectionでつながっていることを確認します。

JDBC を使って接続するようですね。

image.png

データの表示

色々な形式でテーブルを表示することが出来ます。

Table

image.png

Tree

image.png

Text

image.png

フィルタリング

行のフィルタリングも簡単にGUIで実行できます。

image.png

クエリの実行

IDE上でSQLのエディターを開き、クエリを実行することができます。

image.png

インポート/エクスポート

インポートはCSVSQLファイル、表データから行うことが出来ます。

エクスポートはCSVSQL Scriptsなど形式を選んでエクスポートすることが可能です。 image.png

https://pleiades.io/help/webstorm/relational-databases.html

おわりに

今回は、私が WebStorm で愛用している機能を紹介しました。 IDE だけで完結でき、様々なツールを行き来する必要がないので、大変開発しやすいと感じています。

この記事では書いていませんが、他にもエディターの機能や、テスト機能、スクリプト実行など便利な機能が備わっています。

ぜひ、WebStorm に触れてみてください!