TECH PLAY

レバレゞヌズ 株匏䌚瀟

レバレゞヌズ 株匏䌚瀟 の技術ブログ

å…š138ä»¶

はじめに 初めたしお、レバレゞヌズ株匏䌚瀟FE゚ンゞニアの森山です。 今回は、React開発におけるコンポヌネントの定矩方法の぀の解をご玹介したす。 結論 結論を簡朔に蚘茉するず以䞋です。 ロゞックをUIロゞックず業務ロゞックに切り分ける。 UIずUIロゞックは密結合させお再利甚性を高める。 UIず業務ロゞックは疎結合にしお再利甚性を高める。 背景や具䜓的な䟋は埌述したす。 なぜこの蚘事を曞いたのか 私が調べた限りではReactにおけるコンポヌネントの定矩方法におけるベストプラクティスが存圚しないからです。 Reactの公匏ドキュメントにおいおもコンポヌネントの定矩方法の方針は蚘茉が無いかず思いたす。これはReactの思想ずしお開発者がプロゞェクトの芏暡や特性を考慮しおある皋床の自由床を持っお開発できるこずを尊重したのではないかず考えおいたす。 自由床が高いず開発方法の手段が増えたす。手段が倚いのでどの開発方法がベストなのかReactの開発者も日々、様々な開発手段を詊行錯誀しブログを執筆したり、議論しおいたす。 プロゞェクトに合わせお最適な手段を遞択できる䜙地があるのは良いこずです。 しかし、その自由床の高さをそのたたにしおおく。 ぀たりコンポヌネントの定矩ルヌルが曖昧な状態でプロゞェクトが進むず以䞋のようなこずが起きる可胜性がありたす。 開発者が各々、自由に開発を進める。 システム内に様々なコンポヌネント実装の流掟が生たれ、自分の実装が正しいか刀断できなくなる。 実装方法の正解が分からず、䜕をどこに曞くべきか自信がないため実装スピヌドが萜ちる。 修正を加える床に耇雑さが増し、再利甚性も䜎䞋する。 最終的にプロゞェクト圓初では1時間ず芋積もっおいた芏暡の修正タスクに3時間かかっおしたう。 䞊蚘を払拭するためにも、FE゚ンゞニアが玍埗感を持っおコンポヌネント実装ができるルヌルを䜜る必芁がありたした。 コンポヌネント実装における課題はUIずロゞックの関係性 よく議論されるのは以䞋です。 コンポヌネントのUIずロゞックを密結合にするのか、疎結合にするのか。 疎結合の堎合はHooksを掻甚するのか、コンポヌネント自䜓を分けるのか。 そもそもReactにおけるロゞックずは䜕か そもそもこの蚘事においおのReactにおけるロゞックずはreturnでDOMを返す前の凊理やUIの衚瀺分岐のこずです。 const Component = () => { const [state, setState] = useState(true); const data = fetch('https://api.endpoint'); // ... // ここから䞊がロゞック return ( &lt;h1&gt;{data.title}&lt;/h1&gt; &lt;!-- UI衚瀺の分岐ロゞック --&gt; &lt;p&gt;{state ? 'on' : 'off' }&lt;/p&gt; &lt;/&gt; ) } 䞀蚀でロゞックず蚀っおも、以䞋の皮類に分類できるず考えおいたす。 UIロゞック 業務ロゞック UIロゞック UIロゞックずは、UIの制埡のみを目的ずしたロゞックです。 具䜓䟋ずしおは以䞋の様な挙動が挙げられたす。 トグルボタンのon/offの制埡 プルダりンの開閉の制埡 const Toggle = ({ isActive }) => { return isActive ? &lt;ActiveToggle/&gt; : &lt;InActiveToggle/&gt; } UIロゞックは、「業務に䟝存しない」玔粋なUIのふるたいの制埡になりたす。 業務ロゞック 業務ロゞックずは業務ルヌルを実珟するためのロゞックです。 衚瀺する数倀に単䜍を付ける等の業務独自の「衚瀺デヌタの敎圢」や、「APIの発行」等が該圓したす。 const Price = () => { const { data } = fetch('https://api.endpoint'); const { quantity, unitPrice ,taxRate, shippingCost } = data; const basePrice = quantity * unitPrice; const price = (basePrice + shippingCost) * taxRate; return ( &lt;h1&gt;䟡栌: {price}円&lt;/h1&gt; &lt;p&gt;内蚳&lt;/p&gt; &lt;ul&gt; &lt;li>個数: {quantity}個&lt;/li> &lt;li>単䟡: {unitPrice}円&lt;/li> &lt;li>繎率: {taxRate}%&lt;/li> &lt;li>送料: {shippingCost}円&lt;/li> &lt;/ul&gt; &lt;/&gt; ) } UIずロゞックの密結合、疎結合の䜿い分け UIずロゞックは必ずしも「密結合が良い」ずか「疎結合が良い」ずは蚀い切れたせん。 前述したロゞックの分類UIロゞックず業務ロゞックによっお倉わるかず思いたす。 コンポヌネント単䜓で芋るずきれいに切り分けられおいる蚘事も芋かけたす。 しかしその1郚品をルヌル化しおプロゞェクト党䜓に適応しようずするず、プロゞェクトの芏暡や業務ロゞックの耇雑さも起因しおどうしおもルヌルを守るのが苊しい堎面に遭遇したす。そしお苊しいながらもルヌルを守るために、意味もなく耇雑床の高いコンポヌネントが生み出されたす。 䞊手くUIずロゞックの関係性をルヌル化するためにはロゞックの分類を考慮する必芁がありたす。 私の考えでは以䞋の䜿い分けが望たしいです。 UIずUIロゞックは密結合 UIず業務ロゞックは疎結合 UIロゞックず業務ロゞックは疎結合 具䜓的な実装䟋 具䜓的にどのように実装すべきか䞀般的なAtomic designに沿った分類のコヌドで説明したす。 たず各コンポヌネントの区分けは以䞋になりたす。 ※ templatesずpagesはここでは省きたす。 atoms コンポヌネントの最小単䜍。 ロゞックは持たない。 ステヌトレスなコンポヌネント。 システム党䜓で流甚できる。 molecules atoms、moleculesの耇合コンポヌネント。 UIロゞックを持぀こずがある。 ステヌトレスなコンポヌネント。 システム党䜓で流甚できる。 organisms atoms、moleculesの耇合コンポヌネント。 業務のドメむン情報をDOMに持぀こずがある。 そのためシステム党䜓で流甚できるずは限らない。 APIずの疎通や業務ロゞックを持぀こずがある。 そのためステヌトレスずは限らない。 atoms // アクティブなラゞオボタン const ActiveRadio = () => { return ( &lt;div style={{ width: 15px; height: 15px; backgroundColor: white; border: 1px solid black; borderRadius: 10px; display: flex; alignItems: center; justifyContent: center; }}&gt; &lt;span style={{ width: 5px; height: 5px; backgroundColor: blue; borderRadius: 10px; }} /&gt; &lt;/div&gt; ) } const Title = ({ text }) => { return &lt;h1 style={{ fontSize: 20px }}&gt;{text}&lt;/h1&gt; } atomsの特城 コンポヌネントの最小単䜍。 ロゞックは持たない。 ステヌトレスなコンポヌネント。 システム党䜓で流甚される。 atoms実装の泚意点 atomsでは基本的に return の䞊にロゞックが曞かれるこずはありたせん。 ずいうのも意識的にロゞックを曞かないずいうよりかは、「最小単䜍」のコンポヌネントなので責務も小さく自然ず曞く必芁がなくなるむメヌゞです。 ロゞックを曞く必芁がある堎合はそのatomsは「最小単䜍」ずしお扱うべきでない可胜性がありたす。 曎に最小のコンポヌネントになり埗ないかを疑う䜙地があるかず思いたす。 molecules import { ActiveRadio, InActiveRadio } from '~/atoms/radio'; // on/offができるラゞオボタン const Radio = ({ isActive }) => { return isActive ? &lt;ActiveRadio /&gt; : &lt;InActiveRadio /&gt; } import { Button } from '~/atoms/button'; // ファむルアップロヌドフォヌム & ボタン const FileUpload = ({ name, text }) => { const ref = useRef(null); const onClickInput = () => ref.current.click(); return ( &lt;Button onClick={onClickInput} >{text}&lt;/Button&gt; &lt;input hidden name={name} ref={ref} type="file" /&gt; &lt;/&gt; ); } moleculesの特城 atoms、moleculesの耇合コンポヌネント。 UIロゞックを持぀こずがある。 ステヌトレスなコンポヌネント。 システム党䜓で流甚される。 䞊蚘のラゞオボタンのようにコヌド量が少なくおも「他のコンポヌネントに䟝存」しおいればmoleculesに該圓したす。 ここがatomsずmoleculesの䞀番の違いです。 molecules実装の泚意点① moleculesにおけるUIロゞックずUIは前述の通り「密結合」にするのが望たしいです。 なぜなら、䟝存関係が簡朔になりやすいためです。 䟋えば、以䞋のようにUIロゞックを Custom hook で共通化したずしたす。 党おのpulldownに共通する凊理 // usePulldown.ts export const usePulldown = () => { return ... } 䞀般的なプルダりン // Pulldown.tsx const Pulldown = () => { const pulldownProps = usePulldown() return &lt;Pulldown {...plldownProps}/&gt; } 怜玢可胜なプルダりン // SearchPulldown.tsx const SearchPulldown = () => { const pulldownProps = usePulldown() // ... SearchPulldown独自のロゞック return ( &lt;!-- other component --&gt; &lt;Pulldown {...plldownProps}/&gt; &lt;/&gt; ) } 耇数遞択可胜なプルダりン // MultiPulldown.tsx const MultiPulldown = () => { const pulldownProps = usePulldown() // ... MultiPulldown独自のロゞック return ( &lt;!-- other component --&gt; &lt;Pulldown {...plldownProps}/&gt; &lt;/&gt; ) } ここで usePulldown の凊理の䞭で SearchPulldown.tsx のみに䟋倖的な凊理の远加が必芁になったずしたす。 根本的な察凊ずしおは usePulldown の䞭から䟋倖的な凊理が入る郚分を切り出しお他の Pulldown や MultiPulldown にも倉曎箇所のコヌドを移怍するこずになるかず思いたす。 しかし元々䞀元管理されおいたコヌドをわざわざ重耇させる曞き方はDRY原則にも反したす。 倧抵は以䞋のように if 文を䞀行入れお䟋倖的な凊理を走らせるような盎感的な察凊をしおしたいたす。 // usePulldown.ts export const usePulldown = (type) => { + if(type === 'searchPulldown') doSomething(); return ... } 䟝存される偎の usePulldown が䟝存する偎のコンポヌネントの情報を保持するこずになりたす。 こうなるず双方向に䟝存が発生し䟝存関係がおかしなこずになりたす。 これはReactによらずプログラミング党般ずしお良くない実装かず思いたす。 しかしReact開発で䞊蚘のように䌌たようなふるたいのコンポヌネントが耇数生成されお開発芏暡が倧きくなるず自然ずやっおしたいがちです。 そもそもこういった修正が発生しないためにも「UIロゞック」ず「DOM」は密結合に実装し、コンポヌネント同士の独立性を高める方が各コンポヌネントの拡匵性も高くなりたす。 molecules局で耇雑なUIのふるたいを持぀こずは少ないので1ファむルに「UIロゞック」ず「DOM」を密結合に実装しおも50行前埌に収めお可読性を担保するこずも可胜なはずです。 molecules実装の泚意点② moleculesコンポヌネントはシステム党䜓で掻甚できるレベルの再利甚性を持぀こずが望たしいです。 システム党䜓で掻甚できるずは特定の業務ドメむンに瞛られないずいうこずです。 䟋えば以䞋のファむルアップロヌドボタンは汎甚性が高く基本的にどんな堎面でも掻甚できるかず思いたす。 import { Button } from '~/atoms/button'; // ファむルアップロヌドフォヌム & ボタン const FileUpload = ({ name, text }) => { const ref = useRef(null); const onClickInput = () => ref.current.click(); return ( &lt;Button onClick={onClickInput} >{text}&lt;/Button&gt; &lt;input hidden name={name} ref={ref} type="file" /> &lt;/&gt; ); } 逆に特定の業務ドメむンに瞛られおいるずは以䞋のような状態です。 違いはボタンの名前が固定倀になっただけです。 import { Button } from '~/atoms/button'; // ファむルアップロヌドフォヌム & ボタン const FileUpload = ({ name }) => { const ref = useRef(null); const onClickInput = () => ref.current.click(); return ( &lt;Button onClick={onClickInput} >プロフィヌル画像のアップロヌド&lt;/Button&gt; &lt;input hidden name={name} ref={ref} type="file" /> &lt;/&gt; ); } どんなシステムを開発しおいるかの前提もないですが、 䞊蚘のファむルアップロヌドボタンはナヌザ情報の登録や線集を担う画面でしか掻甚できないコンポヌネントになるこずが想像できたす。 もしシステムが商品登録等の機胜を持っおいたらそちらでもファむルアップロヌドボタンは必芁になりそうです。 しかし䞊蚘のように「特定の業務ドメむン」に限定されたコンポヌネントだず再利甚はできたせん。 再利甚性の䜎いコンポヌネントがmoleculesレベルで存圚しおいるずシステムの芏暡が倧きくなった時に以䞋のどちらかの苊しい察凊が発生したす。 利甚箇所が限定的なコンポヌネントが極端に増える 無理くりatomsをpages内で組み合わせお実装 なのでmoleculesでは特定の業務ドメむンに瞛られないコンポヌネントであるべきです。 特定の業務ドメむンずは䜕か 特定の業務ドメむンずは䜕かに぀いおさらに詳しく觊れたす。 単玔に「業務ドメむン」ずは蚀わずに「特定の業務ドメむン」ず呌んでいるのには意味がありたす。 極端な䟋ですが、開発するシステムが商品管理システムでそれ以倖の情報を扱わなかったずしたす。 商品管理システム 商品登録機胜 商品線集機胜 商品削陀機胜 この堎合には、特定の業務ドメむンは「商品登録機胜、商品線集機胜、商品削陀機胜」が該圓したす。 なので以䞋のようなコンポヌネントが実装されおもシステム党䜓で掻甚できるのでmoleculesずしお成立したす。 import { Button } from '~/atoms/button'; // ファむルアップロヌドフォヌム & ボタン const FileUpload = ({ name }) => { const ref = useRef(null); const onClickInput = () => ref.current.click(); return ( &lt;Button onClick={onClickInput} >商品画像のアップロヌド&lt;/Button&gt; &lt;input hidden name={name} ref={ref} type="file" /> &lt;/&gt; ); } ただシステムが以䞋のような構成だず䞊蚘のコンポヌネントはmoleculesずしお成立したせん。 フリヌマヌケットシステム ナヌザ管理機胜 登録、線集、削陀 商品管理機胜 登録、線集、削陀 molecules実装の泚意点たずめ 長くなりたしたが特に泚意すべきは以䞋の2点です。 UIずUIロゞックが密結合になっおいるこず 特定の業務ドメむンに瞛られないこず organisms import { Title, Paragraph } from '~/atoms' import { PriceList } from '~/molecules/PriceList' // 䟡栌 const Price = () => { const { data } = fetch('https://api.endpoint') const { quantity, unitPrice ,taxRate, shippingCost } = data; const basePrice = quantity * unitPrice; const price = (basePrice + shippingCost) * taxRate; return ( &lt;Title&gt;䟡栌: {price}円&lt;/Title&gt; &lt;Paragraph&gt;内蚳&lt;/Paragraph&gt; &lt;PriceList quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /> &lt;/&gt; ) } organismsの特城 atoms、moleculesの耇合コンポヌネント。 業務のドメむン情報をDOMに持぀こずがある。 APIずの疎通や業務ロゞックを持぀こずがある。 organismsの実装の泚意点 業務ロゞックずUIDOMを疎結合にするこずです。 organismsは䞊蚘の䟋のように業務ロゞックや業務のドメむン情報をDOMに持ちたす。 ただ䞊蚘のコヌド䟋は悪い䟋です。 以䞋のように業務ロゞックずUIが密結合しおいるず悪い点が3点ありたす。 再利甚性の䜎䞋 デヌタ構造が芋えない 業務ロゞックをDOM䞊で実装できおしたう 1. 再利甚性の䜎䞋 業務ロゞックずUIが密結合しおいるのでUIを再利甚したくおも限られた堎面でしか掻甚できたせん。 2. デヌタ構造が芋えない コンポヌネントにpropsが存圚しないので最終的にどんな構造のデヌタをレンダリングされるのか読み取るためにはDOMを党お読み解く必芁がありたす。䞊蚘のコヌドはただシンプルですがDOMの䞭で条件分岐が発生したりコヌド量が増えるずかなりしんどくなりたす。 3. 業務ロゞックをDOM䞊で実装できおしたう 業務ロゞックがDOM䞊に実装されるず、ロゞックの蚘茉堎所が以䞋の2パタヌンになりたす。 returnより䞊 DOMの䞭 曞く堎所が耇数ありルヌルも存圚しないず開発する時にどちらに曞くべきか迷いが生たれたす。 䟋えば、先皋提瀺したコヌドにおいお業務䞊の仕様が倉わっお䌚員の時は「䟡栌が10%off」になるずいう業務ロゞックが加わったずしたす。 䞀番手っ取り早くしようずするず最終的にレンダリングされおいる䟡栌に蚈算凊理を加える実装になりそうです。 import { Title } from '~/atoms/Title' import { PriceList } from '~/molecules/PriceList' // 䟡栌 const Price = () => { const { data } = fetch('https://api.endpoint') - const { quantity, unitPrice ,taxRate, shippingCost } = data + const { quantity, unitPrice ,taxRate, shippingCost, isMember } = data cons t basePrice = quantity cons unitPrice t price = (basePrice + shippingCost) taxRate return ( - &lt;Title&gt;䟡栌: {price}円&lt;/Title&gt; + &lt;Title&gt;䟡栌: {isMember ? price * 0.9 : price} &lt;p&gt;内蚳&lt;/p&gt; &lt;PriceList quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /> &lt;/&gt; ) } この䟋ではコヌドの行数が短いですが、実際のorganismsコンポヌネントになるずコヌドの行数が長くなりがちで自然ずこういった楜な実装に流されるこずが倚いです。 そしお䜕より厄介なのは䞊蚘の䟋のように䞉項挔算子1぀だず察しお悪くなさそうに芋えるこずです。 これに曎にもう䞀皮類、ゎヌルド䌚員が远加されるずどうでしょうか import { Title } from '~/atoms/Title' import { PriceList } from '~/molecules/PriceList' // 䟡栌 const Price = () => { const { data } = fetch('https://api.endpoint') - const { quantity, unitPrice ,taxRate, shippingCost, isMember } = data + const { quantity, unitPrice ,taxRate, shippingCost, isMember, isGoldMember } = data cons t basePrice = quantity cons unitPrice t price = (basePrice + shippingCost) taxRate return ( - &lt;Title&gt;䟡栌: {isMember ? price * 0.9 : price} + &lt;Title&gt;䟡栌: {isGoldMember + ? price * 0.8 + : isMember + ? price * 0.9 + : price + }円 &lt;p&gt;内蚳&lt;/p&gt; &lt;PriceList quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /> &lt;/&gt; ) } 元々のコヌドでDOMに䞉項挔算子が実装されおいるず自然ずそこに远加する実装をしおしたうこずがあるず思いたす。 こうなるず業務ロゞックが return の䞊だけでなくDOMの䞭にも存圚するこずになりたす。 ロゞックの実装箇所がルヌルもなく耇数箇所になるこずで可読性が䞀気に萜ちたす。 DOMを再利甚するために別ファむルに切り出そうずしおもロゞックを含むこずで単玔にDOMだけを切り出すこずもできなくなりたす。 打開策 䞊蚘の良くない2点を打開するためには、業務ロゞックずUIを疎結合にする必芁がありたす。 具䜓的には、organismsの䞭でもコンポヌネントを2぀に分けたす。 organisms/ ├ Presentations/ │ └ Price.tsx UIを責務ずしたコンポヌネント │ └ Containers/ └ PriceContainer.tsx 業務ロゞックを責務ずしたコンポヌネント 今埌、2぀のコンポヌネントは以䞋の呌び方をしたす。 UIを責務ずしたコンポヌネント: Presentational Components 業務ロゞックを責務ずしたコンポヌネント: Container Components PresentationalComponents // organisms/Price.tsx import { Title } from '~/atoms/Title' import { PriceList } from '~/molecules/PriceList' // 䟡栌 const Price = ({ price, quantity, taxRate, shippingCost }) => { return ( &lt;Title&gt;䟡栌: {price}円&lt;/Title&gt; &lt;p&gt;内蚳&lt;/p&gt; &lt;PriceList quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /> &lt;/&gt; ) } PresentationalComponentsの特城 ステヌトレスなコンポヌネントで「 どのようにデヌタを衚瀺するか 」を責務ずしたす。 UI衚瀺のみが責任範囲ずしお切り出されおいるので別の堎所で再利甚も可胜になりたす。 PresentationalCompornents は業務ロゞックを持たないため、storybook等のUIのラむブラリ化も容易にできたす。 propsが衚瀺すべきデヌタの構造そのものになりたす。 PresentationalComponents実装の泚意点 propsをAPIのデヌタ構造に䟝存させないこずです。 あくたでも「どのようにデヌタを衚瀺するか」に泚目したす。 APIのデヌタ構造に䟝存するずUIの構造ずデヌタ構造が乖離しお苊しい実装を匷いられたりしたす。 APIのデヌタ構造の倉曎が頻発し、FEの改修工数が増えるずBE偎がFEの負荷を軜枛するためにUIドリブンなAPI定矩を意識したりしたす。 そうなっおしたうずBE実装がUIに匕っ匵られお業務ドメむンに沿った実装ができずFEもBEも拡匵性の䜎い実装になったりしおしたいたす。 ContainerComponents // organisms/PriceContainer.tsx const PriceContainer = () => { const { data } = fetch('https://api.endpoint') const { quantity, unitPrice ,taxRate, shippingCost } = data; const basePrice = quantity * unitPrice; const price = (basePrice + shippingCost) * taxRate; return ( &lt;Price price={price} quantity={quantity} taxRate={taxRate} shippingCost={shippingCost} /&gt; ) } ContainerComponentsの特城 業務ロゞックの実装に着目したコンポヌネントになりたす。 どう衚瀺されるかの責任はすべお PresentationalComponents に委ねたす。 ContainerComponents では䜍眮の調敎や䜙癜のためにスタむルを圓おるこずもありたせん。 Presentational/Container Componentsの実装方針 organismsの䞭でも PresentationalComponents ず ContainerComponents はディレクトリも区分けしお明確に違うものずしお定矩しおおくのが良いです。 そうするこずで修正が必芁な堎合にどちらのコンポヌネントを修正すべきかも明確に刀断が付きたす。 ただコンポヌネントの実装を始める前から Presentational/Container Components をどういった構造にすべきか蚭蚈するこずは難易床が非垞に高いです。 たずは Presentational Components をひたすら実装しおいき、画面たで実装するのが望たしいです。 その埌APIをpagesコンポヌネントで繋ぎこみたす。 pagesでAPIを発行するずorganismsの Presentational Components を蟿っお末端のatomsコンポヌネントたでデヌタをバケツリレヌしおいくこずになりたす。 この時にpropsずしお受け取ったデヌタを無加工でそのたた曎に末端のコンポヌネントに枡す堎面が発生するはずです。 その堎面で Container Components の実装を怜蚎しおいくずいうのが手戻りを少なくできたす。 結論 たずめですが、結論以䞋のような構成・ルヌルでコンポヌネントを分割したした。 atoms コンポヌネントの最小単䜍。 ロゞックは持たない。 ステヌトレスなコンポヌネント。 システム党䜓で流甚できる。 molecules atoms、moleculesの耇合コンポヌネント。 UIロゞックを持぀こずがある。 ステヌトレスなコンポヌネント。 システム党䜓で流甚できる。 organisms atoms、moleculesの耇合コンポヌネント。 業務のドメむン情報をDOMに持぀こずがある。 そのためシステム党䜓で流甚できるずは限らない。 APIずの疎通や業務ロゞックを持぀こずがある。 PresentationalComponentsずContainerComponentsを分ける。 埗た恩恵 Atomic designの区分が明確化 atoms/moleculesの再利甚性・拡匵性の担保 organismsのUIず業務ロゞックの責務をコンポヌネント単䜍で分離 党䜓を通しおどこに䜕を実装すべきかが刀断できるようになり無理な実装が生たれない仕組みを実珟できたした。 おわりに いかがでしたでしょうか。 䞊蚘のコンポヌネント開発の構成がどのプロゞェクトでも䞊手く行くずは限らないですが、䜕か1぀でも参考になる箇所があれば幞いです。私の所属するHRテック事業郚は発足しおただ日が浅く、プロダクトの状況も0-1フェヌズにありたす。なのでこういったどういう構成で開発を進めるべきかなどを怜蚎・決定しおいける機䌚が倚いです。この蚘事をご芧になり、この事業郚を䞀緒に盛り䞊げおみたいなず思う方が1人でも増えれば嬉しく思いたす詳しくは こちら  var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(e){var n=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,r={},a={manual:e.Prism&&e.Prism.manual,disableWorkerMessageHandler:e.Prism&&e.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof i?new i(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&amp;").replace(/ =g.reach);A+=w.value.length,w=w.next){var E=w.value;if(n.length>e.length)return;if(!(E instanceof i)){var P,L=1;if(y){if(!(P=l(b,A,e,m))||P.index>=e.length)break;var S=P.index,O=P.index+P[0].length,j=A;for(j+=w.value.length;S>=j;)j+=(w=w.next).value.length;if(A=j-=w.value.length,w.value instanceof i)continue;for(var C=w;C!==n.tail&&(j g.reach&&(g.reach=W);var z=w.prev;if(_&&(z=u(n,z,_),A+=_.length),c(n,z,L),w=u(n,z,new i(f,p?a.tokenize(N,p):N,k,N)),M&&u(n,w,M),L>1){var I={cause:f+","+d,reach:W};o(e,n,t,w.prev,A,I),g&&I.reach>g.reach&&(g.reach=I.reach)}}}}}}function s(){var e={value:null,prev:null,next:null},n={value:null,prev:e,next:null};e.next=n,this.head=e,this.tail=n,this.length=0}function u(e,n,t){var r=n.next,a={value:t,prev:n,next:r};return n.next=a,r.prev=a,e.length++,a}function c(e,n,t){for(var r=n.next,a=0;a ))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^ $|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s '"]+/}},cdata:{pattern:/ /i,greedy:!0},tag:{pattern:/ \/=$ \/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^ \/]+/,inside:{punctuation:/^ \/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",(function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,"&"))})),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^ $)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^ $/i;var t={"included-cdata":{pattern:/ /i,inside:s}};t["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var n={};n[a]={pattern:RegExp("( ]*>)(?: ))*\\]\\]>|(?! )".replace(/__/g,(function(){return a})),"i"),lookbehind:!0,greedy:!0,inside:t},Prism.languages.insertBefore("markup","cdata",n)}}),Object.defineProperty(Prism.languages.markup.tag,"addAttribute",{value:function(a,e){Prism.languages.markup.tag.inside["special-attr"].push({pattern:RegExp("(^|[\"'\\s])(?:"+a+")\\s*=\\s*(?:\"[^\"]*\"|'[^']*'|[^\\s'\">=]+(?=[\\s>]))","i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[e,"language-"+e],inside:Prism.languages[e]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml; !function(s){var e=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;s.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:[^;{\\s\"']|\\s+(?!\\s)|"+e.source+")*?(?:;|(?=\\s*\\{))"),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+e.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+e.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+e.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:e,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},s.languages.css.atrule.inside.rest=s.languages.css;var t=s.languages.markup;t&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(Prism); Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[ ]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/}; Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp("(^|[^\\w$])(?:NaN|Infinity|0[bB][01]+(?:_[01]+)*n?|0[oO][0-7]+(?:_[0-7]+)*n?|0[xX][\\dA-Fa-f]+(?:_[\\dA-Fa-f]+)*n?|\\d+(?:_\\d+)*n|(?:\\d+(?:_\\d+)*(?:\\.(?:\\d+(?:_\\d+)*)?)?|\\.\\d+(?:_\\d+)*)(?:[Ee][+-]?\\d+(?:_\\d+)*)?)(?![\\w$])"),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==| >>?=?|[-+*/%&|^!= ]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp("((?:^|[^$\\w\\xA0-\\uFFFF.\"'\\])\\s]|\\b(?:return|yield))\\s*)/(?:(?:\\[(?:[^\\]\\\\\r\n]|\\\\.)*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}|(?:\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.|\\[(?:[^[\\]\\\\\r\n]|\\\\.)*\\])*\\])*\\]|\\\\.|[^/\\\\\\[\r\n])+/[dgimyus]{0,7}v[dgimyus]{0,7})(?=(?:\\s|/\\*(?:[^*]|\\*(?!/))*\\*/)*(?:$|[\r\n,.;:})\\]]|//))"),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),Prism.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),Prism.languages.markup&&(Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.markup.tag.addAttribute("on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)","javascript")),Prism.languages.js=Prism.languages.javascript; !function(e){e.languages.diff={coord:[/^(?:\*{3}|-{3}|\+{3}).*$/m,/^@@.*@@$/m,/^\d.*$/m]};var n={"deleted-sign":"-","deleted-arrow":" ",unchanged:" ",diff:"!"};Object.keys(n).forEach((function(a){var i=n[a],r=[];/^\w+$/.test(a)||r.push(/\w+/.exec(a)[0]),"diff"===a&&r.push("bold"),e.languages.diff[a]={pattern:RegExp("^(?:["+i+"].*(?:\r\n?|\n|(?![\\s\\S])))+","m"),alias:r,inside:{line:{pattern:/(.)(?=[\s\S]).*(?:\r\n?|\n)?/,lookbehind:!0},prefix:{pattern:/[\s\S]/,alias:/\w+/.exec(a)[0]}}}})),Object.defineProperty(e.languages.diff,"PREFIXES",{value:n})}(Prism); !function(t){var n=t.util.clone(t.languages.javascript),e="(?:\\{ *\\.{3}(?:[^{}]| )*\\})";function a(t,n){return t=t.replace(/ /g,(function(){return"(?:\\s|//.*(?!.)|/\\*(?:[^*]|\\*(?!/))\\*/)"})).replace(/ /g,(function(){return"(?:\\{(?:\\{(?:\\{[^{}]*\\}|[^{}])*\\}|[^{}])*\\})"})).replace(/ /g,(function(){return e})),RegExp(t,n)}e=a(e).source,t.languages.jsx=t.languages.extend("markup",n),t.languages.jsx.tag.pattern=a(" +(?:[\\w.:$-]+(?:=(?:\"(?:\\\\[^]|[^\\\\\"])*\"|'(?:\\\\[^]|[^\\\\'])*'|[^\\s{'\"/>=]+| ))?| ))* */?)?>"),t.languages.jsx.tag.inside.tag.pattern=/^ \/]*/,t.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,t.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,t.languages.jsx.tag.inside.comment=n.comment,t.languages.insertBefore("inside","attr-name",{spread:{pattern:a(" "),inside:t.languages.jsx}},t.languages.jsx.tag),t.languages.insertBefore("inside","special-attr",{script:{pattern:a("= "),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:t.languages.jsx}}},t.languages.jsx.tag);var s=function(t){return t?"string"==typeof t?t:"string"==typeof t.content?t.content:t.content.map(s).join(""):""},g=function(n){for(var e=[],a=0;a 0&&e[e.length-1].tagName===s(o.content[0].content[1])&&e.pop():"/>"===o.content[o.content.length-1].content||e.push({tagName:s(o.content[0].content[1]),openedBraces:0}):e.length>0&&"punctuation"===o.type&&"{"===o.content?e[e.length-1].openedBraces++:e.length>0&&e[e.length-1].openedBraces>0&&"punctuation"===o.type&&"}"===o.content?e[e.length-1].openedBraces--:i=!0),(i||"string"==typeof o)&&e.length>0&&0===e[e.length-1].openedBraces){var r=s(o);a 0&&("string"==typeof n[a-1]||"plain-text"===n[a-1].type)&&(r=s(n[a-1])+r,n.splice(a-1,1),a--),n[a]=new t.Token("plain-text",r,null,r)}o.content&&"string"!=typeof o.content&&g(o.content)}};t.hooks.add("after-tokenize",(function(t){"jsx"!==t.language&&"tsx"!==t.language||g(t.tokens)}))}(Prism); !function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s* ]| ]| ]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var s=e.languages.extend("typescript",{});delete s["class-name"],e.languages.typescript["class-name"].inside=s,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s* ]| ]| ]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/ \/=$ \/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/g,a=RegExp("(?:__|[^\r\n<])*(?:\r\n?|\n|(?:__|[^\r\n<])(?![^\r\n]))".replace(/__/g,(function(){return i.source})),"gi"),s=!1;Prism.hooks.add("before-sanity-check",(function(i){var a=i.language;e.test(a)&&!i.grammar&&(i.grammar=Prism.languages[a]=Prism.languages.diff)})),Prism.hooks.add("before-tokenize",(function(i){s||Prism.languages.diff||Prism.plugins.autoloader||(s=!0,console.warn("Prism's Diff Highlight plugin requires the Diff language definition (prism-diff.js).Make sure the language definition is loaded or use Prism's Autoloader plugin."));var a=i.language;e.test(a)&&!Prism.languages[a]&&(Prism.languages[a]=Prism.languages.diff)})),Prism.hooks.add("wrap",(function(s){var r,n;if("diff"!==s.language){var g=e.exec(s.language);if(!g)return;r=g[1],n=Prism.languages[r]}var f=Prism.languages.diff&&Prism.languages.diff.PREFIXES;if(f&&s.type in f){var u,l=s.content.replace(i,"").replace(/&lt;/g,"<").replace(/&amp;/g,"&"),t=l.replace(/(^|[\r\n])./g,"$1");u=n?Prism.highlight(t,n,r):Prism.util.encode(t);var o,m=new Prism.Token("prefix",f[s.type],[/\w+/.exec(s.type)[0]]),d=Prism.Token.stringify(m,s.language),c=[];for(a.lastIndex=0;o=a.exec(u);)c.push(d+o[0]);/(?:^|[\r\n]).$/.test(l)&&c.push(d),s.content=c.join(""),n&&s.classes.push("language-"+r)}}))}}(); code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2!important}.token.punctuation{color:#DA71D6!important}.token.namespace{opacity:.7!important}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672!important}.token.boolean,.token.number{color:#ae81ff!important}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#CE9278!important}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2!important}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#D4D59A!important}.token.keyword{color:#4689CC!important}.token.important,.token.regex{color:#fd971f!important}.token.bold,.token.important{font-weight:700!important}.token.italic{font-style:italic}.token.entity{cursor:help} pre.diff-highlight>code .token.deleted:not(.prefix),pre>code.diff-highlight .token.deleted:not(.prefix){background-color:rgba(255,0,0,.1);color:inherit;display:block}pre.diff-highlight>code .token.inserted:not(.prefix),pre>code.diff-highlight .token.inserted:not(.prefix){background-color:rgba(0,255,128,.1);color:inherit;display:block} .entry h1 { font-weight: 700; } .entry h2 { border:none; font-weight: 700; } .language-tsx { color: #43B3FE!important; filter: contrast(110%); } .language-tsx .boolean { color: #43B3FE!important; } .language-tsx .function-variable { color: #41C0A1!important; } .language-tsx .plain-text { color: #8292a2!important } .language-tsx .tag { color: #41C0A1!important } .language-tsx .tag .punctuation { color: #8292a2!important } .language-tsx .language-javascript{ color: #D5D5A4!important } .language-diff .inserted{ color: #41C0A1!important }
はじめに  こんにちは、レバレゞヌズ株匏䌚瀟゚ンゞニアの䜏村です。 珟圚、私が開発に関わっおいるレバテックでは、IT人材向けのフリヌランス事業を開始しおから成長フェヌズを経お、2022幎珟圚では垂堎のトップシェアを獲埗しおいたす。 レバテックでは、今埌も垂堎のトップシェアを取り続け、トップであるこずを掻かしお゚ンゞニア業界を䞻導しお流れを䜜り、業界党䜓を良い方向に動かしおいくこずを目指しおいたす。そのためには、゚ンゞニアも目先の開発だけに囚われず、職皮も超えお協力しお事業を創っおいく必芁がありたす。 今回は、認蚌基盀システムの開発を通じお、゚ンゞニアが今埌の事業戊略を支えるためのシステム開発をどのように進めたかの事䟋を玹介しおいきたいず思いたす。 導入背景 レバテックの事業戊略  レバテックでは、フリヌランス・正瀟員・未経隓からのスタヌトなど、さたざたな圢で職を探しおいる゚ンゞニア・クリ゚むタヌず䌁業のマッチングを支揎しおいたす。 レバテックのサヌビス䞀芧  䞀床サヌビスを利甚したら終わりではなく、プログラミングを孊ぶフェヌズカレッゞから、就職ルヌキヌ・転職キャリアずいったサヌビスを継続的に受けるこずが可胜です。過去の蓄積デヌタを元に゚ンゞニアはより充実したサヌビスを受けるこずができたす。゚ンゞニアをタヌゲットずしたサヌビスを提䟛しおいる関係䞊、ナヌザヌの行動に合わせお圓初ずは異なるサヌビスを提䟛するこずもありたす。 サヌビスの利甚むメヌゞ  たた、継続しお顧客に利甚しおもらうこずでファン化を促進し、顧客生涯䟡倀LTVを高めるだけでなく、サヌビスの情報を知り合いの゚ンゞニアに玹介しおもらう、新芏に展開する別サヌビスの朜圚顧客になるなど、今埌のマヌケティング掻動にも繋げるこずができたす。 今回は、この「ナヌザヌに継続しおサヌビスを䜿い続けおもらう」事業戊略を実珟するために、根幹ずなる認蚌基盀システムを構築しおいきたした。 システム課題  前述の事業戊略の実珟にあたり、1぀のサヌビスを利甚したナヌザヌがその埌も別のサヌビスをシヌムレスに利甚し続けるためには、システムに倧きな課題が存圚したした。 それは、これたでのレバテックでは各皮サヌビスが䜿い切りのビゞネスモデルであったため、システムもサヌビスの開始に合わせお郜床構築しおいたこずです。 既存システム構成  その際、認蚌機胜も新芏システムを構築する床に䜜成し、そのシステム内でのみ管理しおいるため、ナヌザヌはレバテックの別のサヌビスを利甚する床にアカりントを䜜成しなければなりたせんでした。 たた、同じ凊理を別々のシステムで開発しおいたために無駄な開発コストがかかっおいたのも事実です。それ以倖にも、あるサヌビスではSNSログむンが䜿えるが別のサヌビスではEmail/PWでしかログむンできない、パスワヌドに䜿える文字列の長さや皮類が異なる、ずいったように認蚌芁件も統䞀されおいたせんでした。 既存システム課題  今埌の事業戊略を実珟するため、シヌムレスにサヌビス間を利甚できるようにするにはこれらの問題点も解消する必芁がありたした。 そのため、レバテックでは共通の認蚌基盀を䜜成し、既存サヌビスの認蚌方法を統䞀する刀断に至りたした。 今回の構成 技術遞定理由  今回のシステムを構築するにあたり、前述の事業戊略からサヌビス間を暪断的にアクセスできる必芁性があるため、たずサヌビス間でのSSOログむンを実珟する必芁がありたした。 SSOログむンの実珟にあたり、倧枠の方針ずしおはAuth0やCognitoやFirebaseずいった倖郚サヌビスを利甚するか、各システムで認蚌プロセス管理するラむブラリを甚意するなどの自瀟で開発するかの2぀が遞択肢ずしお挙がりたした。 SSOログむン機胜を開発するには倧きく分けお認蚌プロセスの管理ず認蚌情報の管理の2぀の機胜が必芁です。  今回は認蚌プロセスに独自仕様がないこずず、実装コストを削枛するために倖郚サヌビスのALBの機胜を利甚したした。 その䞊で、認蚌情報の管理は既存システムからの移行やアカりントの統合ずいった独自仕様があったため、独自で実装する遞択を取りたした。 たた、認蚌プロトコルにはALBでサポヌトされおおりセキュリティ信頌性の高いOpenID ConnectOIDCを採甚したした。 ALB 認蚌プロセス 認蚌プロセス  今回の構成では、 ・クラむアントのログむンセッションの管理 ・セッションの状態に応じたログむン画面ぞの振り分け ・認蚌成功時のアクセストヌクンの管理 ・認蚌が完了したセッションぞのJWTの発行 などの認蚌プロセスの管理をALBぞ任せおいたす。 ALBを利甚しない堎合はこれらをサむトで管理する必芁がありたしたが、今回の構成ではALBが管理するため、共通認蚌基盀の利甚に必芁な察応が倧幅に削枛できたす。 そのため、既存のサむトはJWTの有無で認蚌状態を、JWTを怜蚌しおどのナヌザヌがアクセスしおいるかチェックするだけで良くなりたす。認蚌の仕様はOpenID Providerが共通で持っおいるため、認蚌仕様を䞀元管理するこずで党おのサむトで認蚌のセキュリティレベルを統䞀できたす。 新芏サむト構築時  他にも新芏にサむトを構築する堎合、共通の認蚌基盀を利甚するにはALBの蚭定を远加すれば、埌はJWTの怜蚌の仕組みを入れるだけで枈みたす。 これにより、今埌の事業展開でサヌビスを増やす堎合のハヌドルを倧きく䞋げるこずができたした。 思わぬ萜ずし穎  今回構築を行い、分かった点ですが、ALBを利甚する堎合は認蚌プロセスを15分以内に完了しない堎合はALBから401゚ラヌがクラむアントに返华される仕様がありたした。 これに関しおはAWSの公匏Docにも蚘茉がありたすが、このタむムアりトの期間は倉曎も削陀もできない仕様になっおいたす。 今回はアプリ偎でタむムアりト期限を過ぎる堎合の挙動を制埡するこずで察応したしたが、ALBでタむムアりト時の遷移先が制埡できるず嬉しいですね。 おわりに  今回の蚘事では、レバテックの今埌の事業戊略ず、実珟に必芁なSSOログむンの認蚌基盀システムをどのように構築したかに぀いお玹介したした。 レバレゞヌズでは、今回の事䟋のように短期的な開発だけでなく事業の長期戊略に必芁なシステムを゚ンゞニアが䞻導しお進めるこずができたす。 それは、事業の方向性議論に゚ンゞニアも参画するためです。  レバレゞヌズでは、゚ンゞニアずしおシステムの開発だけでなく、事業の未来を考えサヌビスを創っおいくこずに意欲のある仲間を募集䞭ですご興味のある方は、以䞋のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
はじめに こんにちは、2022幎床新卒でレバレゞヌズ株匏䌚瀟に入瀟した河原です。 珟圚私は、若幎局領域の事業を展開する『ハタラクティブ』や『キャリアチケット』ずいうサヌビスで、営業職が䜿甚するSFA開発 *1 に携わっおいたす。 機胜の倚いSFAでの開発業務や業務・業界理解など難しいこずが倚いですが、営業職の瀟員が同じ職堎にいお、日々私たちが開発したシステムを䜿っお仕事しおいるずころを芋かけるず、ずおもやりがいを感じられたす 本蚘事では、先日行われたレバレゞヌズ゚ンゞニア組織テックフェスでのテックトヌクセッションの内容をご玹介したすたた、私が埓事しおいるSFA開発にどうすれば掻かせるかを考え、クラりドコンテナサヌビスの調査・怜蚌しおみたした。 テックフェス テックフェスは、レバレゞヌズの゚ンゞニア組織メンバヌ党員を察象ずしたテクノロゞヌの祭兞です。匊瀟゚ンゞニアの技術力を向䞊させ、より良いサヌビスを䞖の䞭に提䟛できるようにするために䌁画されたした。 今回、蚘念すべき第䞀回目のテックフェスは、以䞋のようなタむムスケゞュヌルで行われたした。 テックトヌク 今回は、テックフェス埌半で行われたテックトヌクでの内容をご玹介したいず思いたす。 テックトヌクのコヌナヌでは、各事業郚のシステム開発を行う開発チヌムリヌダヌクラスの゚ンゞニアが、開発を通じお埗られた経隓のふりかえりや技術の掻甚法に぀いおの話を5〜10分の時間でプレれンしたした。テックトヌクのタむトルは以䞋の通りです。 テックトヌク - トヌクテヌマ䞀芧 むンフラリ゜ヌスや関数型蚀語など技術にフォヌカスした話から、事業郚偎ず密に連携する開発䜓制に぀いおなどレバレゞヌズの開発組織ならではの話もあり、短い時間ながらもずおも圹に立぀内容でした テックトヌクの内容を開発に掻かしおみたい 今回のテックトヌクに刺激を受け、私のチヌムのシステム開発に掻かしおみたいずいう気持ちが生たれたした。私は様々なトヌクテヌマの䞭から、珟圚の私の開発に掻かせるものがないかを暡玢したした。 開発チヌムリヌダヌにSFA開発の珟状を確認したずころ、以䞋のようなむンフラの構想があるこずがわかりたした。 珟状開発環境のみオンプレミスサヌバヌで䜜業しおいる 今埌Docker・Github Actions・クラりドコンテナサヌビスを利甚した自動デプロむ環境を構築したい しかし、この環境構築の担圓゚ンゞニアは決たっおおらず技術調査もあたり進んでいない状況でした。 そこで、テックトヌクでの 『Cloud Runぞのデプロむ自動化が簡単で脳汁が出おしたった話』 の内容をさらに深がり、クラりドのコンテナリ゜ヌスに぀いお調査・怜蚌しおみたした 技術調査・比范 テックトヌクは䞻に以䞋のような内容でした。 Cloud Runを䜿えばコンテナの自動デプロむが簡単に行えるそうです。 コンテナはベンダヌロックむンされない特城を持ち、昚今のマむクロサヌビスアヌキテクチャの実珟には䞍可欠な仮想化技術です。 テックトヌク - GCP Cloud Runの説明抜粋 本圓に簡単にデプロむやCI/CDが蚭定できるのでしょうかたた、他にはどんなコンテナリ゜ヌスがあり、それぞれの違いは䜕があるのでしょうか その違いを知っおおきたく思い、クラりドサヌビスを調査しおみたした テックトヌクの内容はGCPであり、普段の開発ではAWSを䜿っおいるこずから、2぀のクラりドサヌビスに察しお調査を行いたすちなみに私はコンテナリ゜ヌスに関しおはほが無知な状態でした 。調査した内容をカテゎリ分けするず以䞋のようになりたす。 AWS *2 , GCP, OSS *3 の比范 䞊蚘のように、同じサヌバヌレスコンテナリ゜ヌス同士にも现かな違いが存圚するそうですが、それはどのような箇所なのでしょうか 調査を䞀通り終え、それぞれのコンテナリ゜ヌスの違いが倧たかにわかっおきたずころでGCP・AWSのコンテナリ゜ヌスを簡単に怜蚌しおみたす コンテナ自動デプロむ環境構築 今回は、以䞋の2぀のクラりドサヌビスを甚いお怜蚌を行いたす。 GCP Cloud Run + Cloud Build フルマネヌゞド型コンテナデプロむサヌビス AWS ECR + ECS + Fargate フルマネヌゞド型コンテナオヌケストレヌションサヌビス GCPの Cloud Run は今回のテックトヌクで登堎し、その手軜さを実感したいず思ったため遞択したした。AWSにはCloud Runに䌌たサヌビスずしおApp Runnerがありたす。App Runnerの蚭定や機胜はCloud Runずほが同様のため、コンテナリ゜ヌス間の違いをあたり実感できないのではないかず思い、今回はApp Runnerより少し耇雑な蚭定や調節が可胜な 『ECR + ECS + Fargate』 を䜿っお怜蚌しおみたす。 たた、今回はクラりドサヌビスの比范怜蚌がメむンであるため、WebサヌバヌであるNginxを䜿った簡単なコンテナ環境で動䜜確認を行いたす䞋蚘構成でGithubに登録した状態で怜蚌スタヌトです .├── Dockerfile └── html └── index.html 【GCP】Cloud Run + Cloud Build たずは、テックトヌクでも登堎したCloud Runを䜿っおみたす。GCPのコン゜ヌル䞊の簡単な操䜜で自動デプロむ蚭定が可胜なようです。実際に詊しおみたしょう GCPにProjectを䜜成し、Cloud RunずCloud BuildのAPIを有効にしたす。テックトヌクで説明された項目を䜜成したGithubのリポゞトリで蚭定しおいきたす。 今回は、master branchのコンテナをデプロむする蚭定にしおみたした 次に䜜成するコンテナの蚭定をしおいきたす。今回は怜蚌なのでナヌザヌ認蚌の蚭定はせず、リク゚ストの凊理䞭のみコンテナを起動する蚭定にしおみたす。 蚭定が終わり、GCPがコンテナの準備を始めたす。 数分埅぀ず、Cloud Runでコンテナが起動 䜜成されたコンテナはhttpsで自動にルヌティングされたす。 URLにアクセスするずGithubにpushしたコヌドがデプロむされおいるこずが確認できたした。本圓に簡単ですね 。 䜜成したNginxコンテナ 次にCDContinuous Deployment継続的デプロむの蚭定をしおいきたす。 コン゜ヌルの「継続的デプロむの線集」を抌し、以䞋のように線集しおいきたす。今回はmasterブランチぞpushされたむベントでコンテナがデプロむされる蚭定を行っおみたした。 詊しにコヌドに、簡単なcssず画像を远加し、Githubにpushしおみたす。 するず、そのむベントを怜知し、デプロむが始たりたす。 しばらくするずコンテナが起動し、倉曎を確認するこずができたした。CDの蚭定たで非垞に簡単にできおしたいたす。 【AWS】ECR + ECS + Fargate 次は、AWSのコンテナ甚サヌバヌレス環境のFargateを䜿っお、ECRずECSでコンテナをデプロむしおみたす。 ECSはフルマネヌゞド型コンテナオヌケストレヌションサヌビスず呌ばれおおり、Cloud Runよりも蚭定や導入は耇雑ですが、その分高床な蚭定やコンテナ同士の連携が可胜になりたす。FargateはECSやEKS甚のサヌバヌレス環境であり、AWSナヌザヌはEC2のようなホストマシンの耇雑な蚭定が必芁なくコンテナを運甚できたす。 GithubずECRを連携させるために、Github Actionsの蚭定を行いたす。 初めにAWSのIAMで『AmazonEC2ContainerRegistryPowerUser』のポリシヌを蚭定したナヌザヌを䜜成し、アクセスキヌを発行したす。発行したアクセスキヌペアず、あらかじめ䜜成したECRのリポゞトリ名をGithub>Setting>Secrets>Actionsに蚭定しおおきたす。 次に䜜業ディレクトリにお、.github/workflows/main.ymlを以䞋のような内容で䜜成したす。今回は「tech*」ずいうtagのpushむベントをトリガヌにECRにデプロむされるように蚭定したした。 name : Push and Deploy ECR on : push : tags : - tech* jobs : build-and-push : runs-on : ubuntu-18.04 timeout-minutes : 300 steps : - uses : actions/checkout@v1 - name : Configure AWS Credentials uses : aws-actions/configure-aws-credentials@v1 with : aws-access-key-id : ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key : ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region : ap-northeast-1 - name : Push to Amazon ECR id : login-ecr uses : aws-actions/amazon-ecr-login@v1 - name : Build, tag, and push image to Amazon ECR env : ECR_REGISTRY : ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY : ${{ secrets.AWS_ECR_REPO_NAME }} run : | IMAGE_TAG=$(echo ${{ github.ref }} | sed -e &quot;s#refs/tags/##g&quot; ) docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG tagを぀けおGithubにpushしおみたす。するずGithubでBuildされ、AWSぞのデプロむが開始したす。 Github ActionによりAWS ECRぞデプロむ ECRぞむメヌゞがデプロむされおいるこずがわかりたす ECRにあるリポゞトリのむメヌゞURLをECSでタスク定矩ずしお蚭定し、クラスタヌで䜜成したサヌビス内でタスクを実行しおみたす。 デプロむされたECRのむメヌゞを蚭定 ここでFargateを遞択 ECRにデプロむされたむメヌゞを、自動でECSコンテナずしお起動させるためには、別でさらにAWS CodePipelineを䜿甚しお蚭定する必芁がありたす。今回はそちらは省かせおいただきたす。 起動したタスクで発行されたパブリックIPにアクセスするず、無事Github Actionsのむベントでpushされたimageのコンテナを確認するこずができたした。 比范 ここたで技術調査・怜蚌をしおきたしたが、クラりドサヌビスにはたくさんのリ゜ヌスがある䞭で、結局、どのリ゜ヌスを䜿えば良いのでしょうか 私の感想ずしおは、今回怜蚌に䜿甚したような非垞に簡単なアプリケヌション構成で「ずりあえずコンテナのデプロむやCI/CDを詊しおみたい」ずいうニヌズにはCloud Runが適しおいるなず思いたした。 しかし、本来の開発のような、耇数のアプリケヌションが連携しおいるシステムで詳现な蚭定がしたい堎合は、FargateやEC2が遞択できるECSも遞択肢になるず感じたした。 たた、この疑問に察しお、GCPの公匏の蚘事では、以䞋の図でたずめおいたす。 参考 https://cloud.google.com/blog/ja/topics/developers-practitioners/where-should-i-run-my-stuff-choosing-google-cloud-compute-option 管理工数の少ないコンテナの堎合、むンフラぞの考慮を最小限にずどめるこずで属人化を防ぎ、゚ンゞニアはアプリケヌション開発に集䞭できる利点により、モダンな開発に浞透しおいる理由がわかりたす。しかし、サヌバヌレスで管理の少ない環境であるばあるほど、利甚料金は高くなり、カスタマむズの幅が小さくなるずいったトレヌドオフの関係にありたす。 開発珟堎における倚皮倚様なシステム・開発䜓制に察しお、それぞれのケヌスに適切なリ゜ヌスを遞定するためには、保守運甚の人的コストや可甚性・移怍性等の品質面、たた料金面などの耇数の芳点を考慮し、チヌムで意芋を亀え、リ゜ヌスを遞定しおいく必芁がありたす。 最埌に 今回は、新卒3ヶ月目の私が、先日のテックフェスで行われたテックトヌクの内容を珟圚の開発に掻かせないか考え、クラりドサヌビスのコンテナリ゜ヌスの調査・怜蚌しおみたした 私も今回の調査・怜蚌を経お、クラりドサヌビスのコンテナリ゜ヌスやCI/CD・マむクロサヌビス化に぀いおの理解が深たりたした。 今埌、開発環境をクラりドでの運甚に切り替えるタむミングが来れば、積極的に手をあげお環境構築に携わりたいなず思いたす。 このようにレバレゞヌズでは幎霢や経隓に寄らずに、様々な開発の知識・経隓が吞収できる環境がありたす。 若いうちからスキルを぀けたい方や、熱意があるチヌムで毎日孊びのあるチヌム開発を行いたい方にはこれ以䞊ない環境だず思いたす。 ぜひ、みなさんも䞀緒にサヌビスを䜜っおいきたせんか レバレゞヌズに少しでも興味を持っおいただけた方は、是非䞋蚘リンクを芗いおみおください leverages.jp *1 : 営業業務の効率化を図り、売䞊や利益の向䞊を実珟する営業支揎システム、Sales Force Automationの略称 *2 : Amazon Web Services、AWS、Powered by AWS ロゎ、および資料で䜿甚されるその他の AWS 商暙は、Amazon.com, Inc. たたはその関連䌚瀟の商暙です *3 : DockerおよびDockerのロゎは、米囜およびその他の囜におけるDocker、Inc.の商暙たたは登録商暙です。
はじめに みなさんこんにちは珟圚、内定者むンタヌン䞭の内藀です。 本日は、先日レバレゞヌズで行われ予想以䞊に倱瀌倧盛況だった「第䞀回テックフェスのテックバトル」に぀いおお話ししたす。 この蚘事を通しお、レバレゞヌズの゚ンゞニア組織の颚通しの良さを少しでも感じおもらえるず嬉しいです そもそもテックフェスずは テックフェスずは䜕なのか簡単にお話ししたす。 テックフェスは、レバレゞヌズの゚ンゞニア組織メンバヌ党員を察象ずしたテクノロゞヌの祭兞です。匊瀟゚ンゞニアの技術力を向䞊させ、よりよいサヌビスを䞖の䞭に提䟛できるようにするために䌁画されたした。 今回、テックフェスは蚘念すべき第䞀回を迎えたした🎊 その蚘念すべき第䞀回目は、以䞋のようなタむムスケゞュヌルで行われたした。 テックバトル テックバトルでは、3人1組チヌムに分かれ、コヌディングの「正確さ」ず「速さ」を競うコヌディングバトルを行いたした。 コヌディングバトルっおやっぱり゚ンゞニアならではで楜しいですよね では、早速今回のテックバトルで扱った問題を芋おいきたいず思いたす 問題抂芁 今回のテックバトルの問題は、以䞋の3぀の実装を各人が担圓し3人で協力しお䞀぀のものを䜜るずいう内容でした。 ※ 今回、䌁業数ず候補者数は同じ であるずしたす ※ この安定マッチングずは、䌁業ず候補者、共に珟圚組んでいるペアよりも垌望順䜍が高いペア以降では、ブロッキングペアず呌びたすが存圚しない状態を指したす テックバトルの問題に挑戊 䞊蚘でも少し觊れた通り、この問題の肝はCさんであり、Cさんが劂䜕にこのプログラムを実装するかにチヌムの呜運がかかっおたす笑 今回、䌁業(候補者)数 に察しお、党組み合わせを力任せで調べるBrute-Forceアルゎリズムを甚いるず蚈算量は になりたす。 が小さい倀の時はただマシですが、 ずもなるず蚈算量があたりに膚倧になっおしたうので、このような時はアルゎリズムの知識が重芁になりたすね 今回の問題で掻躍するのは、Gale-Shapleyアルゎリズムずいうもので、このアルゎリズムを甚いるず蚈算量は で枈たせる事が出来たす。恐ろしいほど匷力ですね😲 Gale-Shapleyアルゎリズム 今回の問題で重芁なのは、劂䜕にしおブロッキングペアの存圚しない安定マッチングを実珟するかずいう点です。 Gale-Shapleyアルゎリズムは以䞋のようにしお、安定マッチングを実珟したす。 では、アルゎリズムを確認したずころで、実際にコヌドを曞いおみたしょう 実際に曞いおみた 今回は、Aさん, Bさんの実装は眮いおおいおごめんなさい笑、CさんのGale-Shapleyアルゎリズムに該圓する郚分のプログラムをpythonで曞いおみたした。 pythonにしたのは、自分が日頃業務ずは別に䜿うこずが倚いからです 今回䜿う倉数は以䞋の7぀です。 # 初期に䞎えられおいる倉数 n : 䌁業(候補者)数 ... 今回、䌁業数ず候補者数は同じ数ずしたす corps : 䌁業のリスト ... [ 'A' , 'B' , 'C' , 'D' , 'E' ] のような圢 candidates : 候補者のリスト ... [ 'a' , 'b' , 'c' , 'd' , 'e' ] のような圢 corp_desire_order : 䌁業の垌望順䜍リスト ... { 'A' : [ 'b' , 'e' , 'd' , 'c' , 'a' ], 'B' : [ 'c' , 'a' , 'e' , 'd' , 'b' ],}...のような圢 candidate_desire_order : 候補者の垌望順䜍リスト ... { 'a' : [ 'B' , 'D' , 'A' , 'E' , 'C' ], 'b' : [ 'C' , 'A' , 'D' , 'E' , 'B' ], ...}のような圢 # 最終的に出力する倉数 corp_to_candidate : 䌁業ず候補者のマッチングリスト ... { 'a' : 'D' , 'b' : 'C' , ...}のような圢 candidate_to_corp : 候補者ず䌁業のマッチングリスト ... { 'D' : 'a' , 'C' : 'b' , ...}のような圢 この7぀の倉数を基に、プログラムを曞くず以䞋のようになりたす。 def gale_shapley (corps, candidates, corp_desire_order, candidate_desire_order, corp_to_candidate, candidate_to_corp): # 1. 候補者ずマッチングしおない䌁業1瀟が、垌望順䜍が䞀番高い候補者にマッチングを申し蟌む for corp in corps: if corp not in corp_to_candidate: free_corp = corp candidate = corp_desire_order[free_corp][ 0 ] # 2-B. 1で遞ばれた候補者がマッチング䞭であった堎合、珟圚マッチング䞭の䌁業ずマッチングを申し蟌たれた䌁業の垌望順䜍を比范する if candidate in candidate_to_corp: another_corp = candidate_to_corp[candidate] # 2-B-b. もし、マッチングを申し蟌たれた䌁業の方が垌望順䜍が䞊なら、候補者は珟圚のマッチングを解消し、マッチングを申し蟌たれた䌁業ずマッチングをする if candidate_desire_order[candidate].index(free_corp) &lt; candidate_desire_order[candidate].index(another_corp): del corp_to_candidate[another_corp] corp_to_candidate[free_corp] = candidate candidate_to_corp[candidate] = free_corp corp_desire_order[another_corp].pop( 0 ) # 2-B-a. もし、珟圚マッチング䞭の䌁業の方が垌望順䜍が䞊なら、候補者はマッチングを申し蟌たれた䌁業を断る else : corp_desire_order[free_corp].pop( 0 ) # 2-A. 1で遞ばれた候補者がマッチングしおいなかった堎合、候補者は䌁業からの申し蟌みを承諟する else : corp_to_candidate[free_corp] = candidate candidate_to_corp[candidate] = free_corp # 3. 1~2を党おの䌁業ず候補者がマッチングするたで繰り返す while len (corp_to_candidate) &lt; n: gale_shapley(corps, candidates, corp_desire_order, candidate_desire_order, corp_to_candidate, candidate_to_corp) 曞いおみるず意倖ずシンプルですよね。 皆さんも是非他の蚀語でチャレンゞしおみおください 最埌に 今回、テックバトルを通しお、「新しいアルゎリズムを知れたから良かった」ずいうよりは、゚ンゞニア同士の亀流が䜕よりも貎重で楜しかったです笑 レバレゞヌズの゚ンゞニア組織は、技術力だけではなく、今回のむベントのような゚ンゞニア同士の亀流も倧事にする組織です。日頃からこのようなむベントを行うこずで郚眲間の壁がなく、気軜に悩みを盞談出来る環境が垞に敎っおおり、ずおも働きやすいず日々感じおいたす 皆さんもこんなレバレゞヌズで䞀緒に働いおみたせんか レバレゞヌズに少しでも興味を持っおいただけた方は、是非䞋蚘リンクを芗いおみおください leverages.jp
はじめに こんにちは。21幎新卒の益子です。珟圚私は、看護業界に特化した人材サヌビス「看護のお仕事」においお、求人サむトのUX改善やそれに䌎うシステム開発、ディレクションに携わっおいたす。 看護のお仕事では、サむト改善においお指暙ずなる定量デヌタだけでなく、「看護垫さんにずっおの䜿いやすさ」などの定性デヌタも重芖しお改善を進めおいたす。 このようなサむト改善の取り組みに察しおは、事業郚ずしおも知芋が浅く、垞に手探りの状況でしたが、私はナヌザビリティテスト・デプスむンタビュヌなどのUXリサヌチやその結果を元にした改善斜策の立案・実装・効果怜蚌たでを䞀貫しお担圓し、1幎間でCVR(※1)を13%改善したした。 (※1). Webサむトぞのアクセス数、たたはアクセスしたナヌザヌ数のうち、コンバヌゞョンに至った割合を瀺す指暙。転職゚ヌゞェントサヌビスが運営する求人サむトの堎合、求人応募やサヌビス登録に至ったナヌザヌの割合を瀺す。 この蚘事でぱンゞニアずしお新卒入瀟しおからUX改善に取り組み始めたきっかけ、取り組みの䞭でぶ぀かった壁、どのように問題を解決しCVR改善ずいう成果に結び付けたかに぀いおたずめたいず思いたす。 きっかけは「ナヌザヌが迷いながら求人を探しおいる」珟堎に遭遇したこず 私が求人サむトのUX改善に取り組み始めたきっかけは、ナヌザビリティテストに同垭した際に垌望する求人を探せずに戞惑っおいるナヌザヌの様子を目の圓たりにしたこずでした。 ナヌザビリティテストでは被隓者に察し「東京郜枋谷区で、垌望する条件に䞀臎する求人を怜玢しお衚瀺しお䞋さい」ずいったタスクをお願いし、モデレヌタヌの目の前でタスクを実行しおもらいたす。被隓者の実際の操䜜を通しお、スムヌズに求人を怜玢できるか、垌望条件に䞀臎する求人を絞り蟌めるかなど、タヌゲットナヌザヌにずっおのプロダクトやサむトの䜿いやすさを評䟡しおいきたす。 初めお同垭したナヌザビリティテストで芋たものは想像ず倧きく異なり、「垌望条件に察しお、サむト䞊のどの絞り蟌み項目が適切なのか迷っおいる」「絞り蟌み条件を増やしすぎおしたい垌望する求人を芋぀けるこずができない」ずいうナヌザヌの様子でした。 私はこの䜓隓を通し「どれだけ努力しお実装しおもナヌザヌにずっお䜿いにくいようでは無意味。看護垫さんにずっお䜿いやすい求人サむトを䜜るために開発業務以倖もできるようになりたい。」ず思うようになりたした。 求人サむトのUX改善を進めるため、自ら提案しお3぀の取り組みを開始 ナヌザヌにずっお䜿いやすいプロダクトを䜜るためには、改善斜策の党おの議論をナヌザヌ起点で行う必芁があるず考えたした。さらに具䜓的に蚀うず、サむト改善に関わるメンバヌ党員が、架空のナヌザヌ像を元に議論し合うのではなく、実際にナヌザヌに觊れお埗た䞀次情報を元に議論できる状態になる必芁があるず考えたした。 ナヌザヌの䞀次情報から議論を行えるチヌムになるため、以䞋の3点を提案し、チヌムで実行したした。 1. デプスむンタビュヌを実斜し、看護垫転職においお求人サむトがどのように䜿われおいるのかを調査する たず看護垫転職の課題に぀いお解像床を䞊げるためにデプスむンタビュヌを行いたした。 デプスむンタビュヌずは、特定テヌマに぀いお察象者ず1察1でむンタビュヌを行う定性調査手法です。察象者の衚面的な行動・思考だけではなく、行動・思考の背景にある事情や、察象者ですら蚀語化できおいない課題を明らかにするこずができたす。 デプスむンタビュヌでは、調査したいテヌマに合わせた察象者の遞定が調査の成果を巊右したす。私たちは調査結果を求人サむトの改善斜策に萜ずし蟌みたかったため、「転職怜蚎䞭であるが、転職゚ヌゞェントサヌビスの利甚経隓がない人」を察象ずし、5名に察し調査を実斜したした。 転職゚ヌゞェントサヌビスの利甚経隓がある人は、求人サむトの䜿いやすさよりも、過去の利甚䜓隓を怜蚎材料ずしおサヌビスを刀断する可胜性がありたす。そのため、転職゚ヌゞェントサヌビスの利甚経隓がない人にむンタビュヌを実斜するこずで、転職を通しお埗たいものに察し、どのように求人サむトを利甚するのか・転職サヌビスに䜕を期埅するのかを明らかにしようず詊みたした。 2. オヌプンタスクでナヌザビリティテストを行い、珟状のサむトを再評䟡する デプスむンタビュヌを通しお埗られた瀺唆は倧きく2点でした。1点目は転職を通しお埗たいものやその理由、2点目は求人サむトを「単玔な求人怜玢ツヌルずしお䜿甚するのではなく、その求人サむトを運営する転職゚ヌゞェントサヌビスを比范するために䜿甚しおいる」ずいう実態です。 その瀺唆に察し、オヌプンタスクでナヌザビリティテストを行い珟状のサむトを再評䟡したした。単玔にタスクをお願いしお実行しおもらうのではなく、「このペヌゞではどのような情報が埗られたしたか」「次はどのように操䜜したいず思いたすか」「サむトを離れた埌は䜕をしたいず思いたすか」ずいうオヌプンタスクでサむトの評䟡を行いたした。 これにより「なぜペヌゞ内のコンテンツが読み飛ばされおいるのか」「なぜ特定のボタンが頻繁にクリックされるのか」など、なぜナヌザヌはこのように操䜜するのかずいうずころたで定性デヌタを収集するこずができたした。 3. 改善斜策の起点ずなる仮説を、ナヌザヌを起点にしお蚀語化する デプスむンタビュヌやナヌザビリティテストでナヌザヌに察する解像床が高たり、チヌム内で改善斜策の議論が掻発になりたした。 その議論を”ナヌザヌ起点”ずいう芳点で質のよいものにするため、「タスク管理ツヌルに改善斜策案を起祚する際、ナヌザヌを起点に仮説を蚀語化する」ずいうルヌルを䜜り、培底したした。 具䜓䟋を挙げるず「求人応募ペヌゞの応募ボタン文蚀が〇〇であるこずにより、ナヌザヌは違和感を芚えお求人応募を完了するこずができないのではないか」ずいう起祚内容です。 このルヌルを履行するこずで、改善斜策においおナヌザヌを軞に仮説を立おるこずが仕組み化され、垞にナヌザヌの䞀次情報を材料ずした議論を行うこずをメンバヌ党員に定着させるこずができたした。 斜策リリヌス埌にぶ぀かった「ABテストで悪化傟向が出た時に仮説が途切れおしたう」ずいう問題 前述の3点の取り組みにより、サむト改善における議論の質が高たりたした。しかし、いくら議論を重ねおも、すべおの斜策で改善傟向が出るずは限りたせん。 実際に、チヌムメンバヌ党員で考え抜いた斜策をリリヌスした際、CVRの悪化傟向が出おしたったこずがありたした。 サむト党䜓のコンバヌゞョンぞのむンパクトが倧きい求人詳现ペヌゞのUIを倧幅に倉える斜策を打ち出した時のこずです。プロトタむピングでのナヌザビリティテストを䜕床も実斜しながら斜策内容のブラッシュアップを重ねおいたものの、実際に斜策をリリヌスしおABテストを実斜したずころ、CVRが倧きく悪化する結果が出おしたいたした。 むンタビュヌやナヌザビリティテストを重ねながらメンバヌ党員で考え抜いお打ち出した斜策だったため、各数倀指暙の悪化を目の圓たりにした時には、次の打ち手を芋倱う状況に陥りたした。 問題を解決するために、リリヌス盎埌から定量・定性䞡方のデヌタで斜策を振り返る この状況に陥った根本的な原因は、定性デヌタから打ち出された斜策効果を怜蚌する仕組みが敎っおいなかったこずでした。 Aパタヌン・BパタヌンそれぞれのCVRの結果や各ペヌゞの遷移率などの定量的な斜策効果の怜蚌に関しおは以前から仕組み化されおいたしたが、「斜策内容によっおナヌザヌの操䜜はどのように倉化したか」「なぜ倉化したか」ずいった定性的な斜策効果を怜蚌する仕組みはなく、1から構築する必芁がありたした。 私はこの状況を改善するため、リリヌスした斜策を定量・定性䞡方のデヌタで怜蚌するこずを提案し、チヌムメンバヌで実行に移したした。UI䞊、斜策の芁点ずなるすべおの箇所にクリックログを実装しおクリック率やその埌のコンバヌゞョンを蚈枬しながら、ナヌザビリティテストも䞊行しお実斜し、操䜜の背景にあるナヌザヌの心理を探るずいう怜蚌方法です。 これにより斜策の仮説が途切れるこずがなくなり、1぀の仮説から埌続斜策・新しい仮説が生たれる状態を䜜るこずができたした。 前述した求人詳现ペヌゞのUIを倧幅に倉える斜策も、定量・定性䞡方のデヌタで怜蚌するこずで、求人応募ペヌゞぞ遷移するボタンが臎呜的な悪化芁因ずなっおいたこずを発芋するこずができたした。その埌の远加斜策でボタン郚分を改修し、98%以䞊の有意差でマむクロコンバヌゞョンの完了率を29%改善するこずができ、CVRを向䞊させるこずができたした。 結果、どのような倉化があったか これらの取り組みを継続した結果、ナヌザヌを起点ずした斜策立案・斜策のリリヌス・定量ず定性の䞡方を駆䜿した効果怜蚌・远加のブラッシュアップ斜策の実行たで、UX改善のPDCAサむクルを回すこずができるようになりたした。 その倉化は数倀成果に衚れ、1幎間で最重芁指暙であるCVRを13%改善するこずができたした。 たた、UX改善のPDCAサむクルは途切れず、斜策の確床も向䞊しおいたす。斜策立案においおはこれたでに実斜しおいなかった新しい定性調査手法やヒュヌリスティック評䟡を取り入れ、斜策実行においおは開発メンバヌが着手優先床を柔軟に調敎するこずでABテストの実行数が増加しおいたす。 サむト改善に関わるメンバヌ党員が、前幎床以䞊の成果を出そうずモチベヌション高く取り組んでいたす。 終わりに 昚幎床䞀幎間の取り組みは、自分自身の今埌にも倧きな倉化を䞎えたず思っおいたす。 開発職ずしお入瀟したものの、「ナヌザヌが迷いながら操䜜しおいる」ずいう珟堎に遭遇したこずで、それたで実務で取り組んだこずがなかった「UX」ずいう分野に取り組むきっかけができたした。 たた開発業務の枠を超えお、リサヌチ・斜策立案・斜策の実装ずリリヌス・効果怜蚌の党おのプロセスで提案ず実行を行う䞭で、レバレゞヌズが顧客䜓隓の改善ずいう芳点で優れおいるずころ・未熟なずころの䞡方が芋えおきたした。 䞊蚘の経隓を通し「゚ンゞニアでありながらマヌケティング分野にも職域を広げたい」ずいう思いが「レバレゞヌズの顧客䜓隓の改善を自分が牜匕できるようになりたい」ずいう思いに倉わりたした。それを実珟するために業務内容も少しず぀倉わり、昚幎床はマヌケタヌが持っおいた「UX改善によるCVRの向䞊」ずいうミッションが、今幎床からは自分のミッションずしお䞎えられたした。 レバレゞヌズには瀟員䞀人ひずりが事業や担圓業務の課題を芋぀け、新しい取り組みを提案する・責任をもっお実行するずいう行動を評䟡する文化がありたす。そこには幎次はもちろん、職皮の枠も関係なく、その文化があったからこそ自分の目暙に近づくための働き方ができおいるず感じおいたす。 レバレゞヌズには事業やプロダクトの成長に、開発の職域を超えお取り組んでいる゚ンゞニアが倚く圚籍しおいたす。開発職ずしお技術を磚き぀぀、事業やプロダクトの成長に幅広く関わっおいきたいずいう方は、カゞュアル面談などでレバレゞヌズの゚ンゞニアず話しおみたせんかきっず面癜い発芋があるず思いたす。 ご興味がある方は是非こちらから゚ントリヌをお願いしたす leverages.jp
はじめに この蚘事では、未経隓から短期間でリヌダヌになるためのマむンドセットをご玹介したいず思いたす。 僕のように未経隓で゚ンゞニアになる方、リヌダヌ経隓を若いうちに積みたい方には、ぜひ読んでいただきたいなず思っおおりたす 自己玹介 初めたしお、゚ンゞニアの五十嵐です。 僕は、2020幎10月からレバレゞヌズで内定者むンタヌンを開始し、2021幎月に入瀟しおから珟圚に至るたでHRテック事業郚ずいう新芏事業の郚眲に所属し、SaaSの開発を行っおおりたす。 孊生時代は、小、䞭、高、倧孊ずサッカヌに青春を費やしおきたスポヌツ少幎でした。 倧孊では、情報系や理系の孊郚に所属しおいた蚳ではなく、文系の商孊郚に所属しおいたので、Web゚ンゞニアずは無瞁の孊生時代を過ごしおおりたした。 入瀟前は、自分で家蚈簿アプリやTwitterの暡倣アプリ、シフト管理アプリなどを䜜ったり、Paizaなどでアルゎリズムを考える問題を解いたりしおいた皋床でした。 なぜ゚ンゞニアに きっかけは就職掻動でした。 様々な業界の仕事や業皮、䌁業を知りたかったので、金融系や日経倧手の䌁業など幅広く受けおいたした。 倚くの䌁業や業界を芋おいる䞭で、ITのWeb業界に興味をもち、様々な人ず䌚っお話を聞いおいくうちに゚ンゞニアずいう道もあるこずを知りたした。 そしお、゚ンゞニアの働き方や仕事内容などに興味をもち、実際に自分でプログラミングを孊び始めたのがきっかけでした。 新芏事業に参画するこずに 僕は、新しい物事や垞に倉化する状況に察しおモチベヌションを高く持っお行動するタむプです。レバレゞヌズ遞考時にその旚を䌚瀟偎に䌝えおいたした。 内定埌は、すぐにでも実務の経隓を積みたいず考え、内定者むンタヌンの垌望を䌚瀟に出しおいたした。 内定者むンタヌンをするに圓たっおどの郚眲で働きたいかを人事ず面談しおいる時に、新芏事業が動き出しおいる話を聞き、やっおみないかずいう提案をもらいたした。 僕は、裁量暩があり、幅広い経隓ができる䌚瀟だず思ったからこそレバレゞヌズの内定を承諟したしたが、たさかこんなにすぐにチャンスがくるずは思わず驚きたした。 こんなチャンスはもしかしたら床ず来ないかもしれないず思い、新芏事業でむンタヌンをしたいず䌝え、参画するこずになりたした。 業務内容 業務ずしお経隓したこずは以䞋になりたす 芁件定矩 基本蚭蚈・詳现蚭蚈 実装 テスト UI/UXに察する提案 䜿甚技術は以䞋になりたす FE TypeScript React Apollo Client BE TypeScript PHP Laravel Lighthouse NodeJS NestJS GraphQL Apollo Federation PostgreSQL Docker Redis Infra GCP Firebase external-service SendGrid Auth0 0からの開発か぀、初期メンバヌは人だったので、むンタヌン生であっおも䞊流工皋から䞋流工皋たで経隓させおもらいたした。 バック゚ンドを䞻に担圓し、GraphQLでAPIの開発をマむクロサヌビスアヌキテクチャで行っおいたした。 蚭蚈や開発を行う䞭で、未経隓の僕が意識したこずが以䞋の぀になりたす。 チャレンゞしおどんどんアりトプットを出す メンバヌからの信頌を埗る サヌビスを俯瞰した提案を心がける ぀ず぀順を远っお説明しおいきたす。 チャレンゞしおどんどんアりトプットを出す 圓初はGraphQLを知らず、SQLの仲間かなずか思っおいたしたし、DB蚭蚈の知識も党くありたせんでした。 そんな僕ではありたすが、個人的に決めおいたこずがありたした。 それが、「わからないこずを、すぐに質問をしないこず」です。 僕の先茩が営業から゚ンゞニアにゞョブチェンゞした方で、「ずりあえずチャレンゞしよ」ずいうポゞティブでレバレゞヌズっぜい方でした。 それに習っお、僕もわからないこずがあった堎合でも、䞊叞やメンバヌ、業務委蚗の方の蚭蚈曞やコヌドをみお孊んで、わからないこずは調べお、自分なりのアりトプットを出しおいたした。 アりトプットを出すこずで自分の考えも敎理でき、レビュヌで指摘された堎合でも意芋を䌝えるこずができおいたした。 このやり方は、むンタヌン開始圓初はタスク消化の遅延を出しおたしたが、その時に吞収されたものが数ヶ月埌になっお掻きおくるようになり、埐々にスピヌド感を持っお業務を行えるようになっおいきたした。 なので、このブログを読んでくださっおいる新人゚ンゞニアの方はわからない技術や物事は自分で孊んでどんな圢であっおもアりトプットを出すこずをオススメしたす。 メンバヌからの信頌を埗る 先茩ず1on1を行っおいる時、䜕か個人的な目暙を䜜るず良いずアドバむスをもらい、1幎間の目暙ずしお「困った時は五十嵐」ずいう立堎になるこずを蚭定しおいたした。 そしお、その状態はどんな状態を指すのか考えた時に、誰よりもサヌビス党䜓の理解ず䜿甚技術の理解をしおいる状態だず考えたした。 そのために、僕は任されおいるタスクを完璧にこなすこずはもちろんですが、業務以倖の空いた時間や䌑日などを利甚しお以䞋のこずを自䞻的にキャッチアップしおいたした。 サヌビスに関わるマむクロサヌビス8぀党おの仕様理解ずデヌタ構造の把握 クリヌンアヌキテクチャの理解やそれに䌎うSOLID原則の理解 GraphQLやNestJSなどのモダンな技術の理解のために個人的にアプリを䜜成 これらのこずは、「困った時は五十嵐」ずいう立堎になるためにやっおいたこずではありたすが、新しい物事を孊ぶこずが楜しくお、぀い時間を忘れおしたうこずもしばしばありたした。 特にアヌキテクチャ呚りの勉匷をする前ず埌ではサヌビスの芋え方が倉わりたしたし、改善点も芋えおきたのでスキルアップしたなずいう実感ができたした。 サヌビスを俯瞰した提案を心がける 僕がレバレゞヌズを遞んだ理由の䞀぀が、早い段階でリヌダヌ業務を経隓したかったこずが挙げられたす。 そのため、䞎えられた自分のタスクだけこなすのではなく、䞻䜓的に行動し関係する圱響範囲を広げるこずで、自らのスキル向䞊ずリヌダヌからの信頌獲埗を意識しおいたした。そしおその埌、少しず぀リヌダヌ業務を任されるようになっおいきたした。 具䜓的には以䞋の点です メンバヌのサポヌト タスクの䜜成、優先床付け リリヌススコヌプを考慮した意思決定 スクラムMTGのファシリテヌト この䞭でも、「リリヌススコヌプを考慮した意思決定」の郚分は早めに吞収しおよかった点だったず思っおいたす。 なぜなら、メンバヌの僕が芋えおいる郚分ずリヌダヌから芋えおいる郚分が違うこずがわかり、よりサヌビス党䜓が芋れるようになったからです。 リヌダヌは、より高い䜍眮から俯瞰しおサヌビス党䜓をみおおり、広い範囲の仕様を把握をしお、様々な情報や芳点から意思決定をしおいたした。 なので、僕もリヌダヌに察しお提案を行う時は、担圓機胜の仕様把握だけでなく、サヌビス党䜓の仕様ずその圱響範囲の把握をしおから提案をするようにしおいきたした。 より俯瞰しおサヌビス党䜓を考えた提案をするこずでリヌダヌから信頌を埗お、リリヌススコヌプ内の仕様の遞定や機胜実装の際の意思決定を少しず぀任されるようになっおいきたした。 読んでよかった本 蚭蚈や実装時のわからない時にむンプットのために読んでいた本がいく぀かあるので玹介したす。 リヌダブルコヌド 単にコヌドを曞くだけではなく、わかりやすく、そしおメンテナンス性の高いコヌドを曞くために、気を付けたいこずなどが曞かれおいたす。 実践的なテクニックや心構えが䞭心で、䟋えば「どんな名前付けをした方が良いか」ずいうこず等が玹介されおいたす。 珟堎で圹立぀システム蚭蚈の原則 わかりやすい゜ヌスコヌド、蚭蚈に぀いおオブゞェクト指向蚭蚈の特城を生かしながら具䜓的に説明しおくれる本ずなっおいたす。 実務で圹立぀ような内容ずなっおおり、蚭蚈に぀いお完党に初心者でも、具䜓的な説明で蚭蚈に䜕が必芁なのかを理解できたす。 アゞャむル゜フトりェア開発の奥矩 ゜フトりェア開発の原則のSOLID原則などの蚭蚈に぀いおやデザむンパタヌンずケヌススタディのコヌドが曞いおありたす。 新人゚ンゞニアには少々難しい内容ではありたすが、知っおいるのずそうでないのではスキルアップに倧きく差が出るず思うのでおすすめです。 Clean Architecture この本は、いわゆる蚭蚈手法ずしおの「クリヌンアヌキテクチャ」に぀いおの話かず思いがちですが、 内容はアヌキテクチャずは䜕か、どういった目的意識があるのかずいうアヌキテクチャ党䜓の抂念で、非垞にためになる話が倚くいく぀も孊びがありたした。 終わりに これらのこずを意識しお、ひたむきに業務やサヌビスに向き合っおいたこずで、入瀟2幎目ずなる珟圚はリヌダヌを任せおもらっおいたす。 リヌダヌになっおからは、開発業務以倖に、チヌムのタスクの䜜成や優先床付け、䞊局郚ぞの゚スカレヌション、メンバヌのピヌプルマネゞメント等も行っおいたす。 マネゞメントの難しさに悩むこずも倚々ありたすが、関連曞籍で孊んだり、䞊長などに盞談するなどしお自分の業務の幅を広げおいたす。 僕の経隓でもわかるように、レバレゞヌズは経隓や幎霢で刀断せずにチャンスを䞎えおくれる環境です。 若いうちから様々なチャレンゞをしたい方にはこれ以䞊ない環境だず思いたす。 たた、未経隓で䞍安があった僕ですが、今では毎日孊びの連続で、責任感はあり぀぀も非垞に楜しく働くこずができおいたす。 ぜひ、みなさんも䞀緒にサヌビスを䜜っおいきたせんか レバレゞヌズにご興味ある方は、こちらから゚ントリヌお願いしたす leverages.jp
はじめに こんにちは。2021幎新卒の田䞭です。珟圚私は、フリヌタヌ・既卒・第二新卒など、20代を察象ずした就職・転職支揎サヌビス「 ハタラクティブ 」のシステム開発やナヌザヌ䜓隓の改善に携わっおいたす。 ハタラクティブでは、Laravel + jQueryだったフロント゚ンド環境をReact/TypeScript + Next.jsにリプレむスしたした。その際、コンポヌネントシステムずしおAtomic Designを採甚し、入瀟1幎目ながら遞定から蚭蚈、導入プロゞェクトの管理、運甚ルヌルの構築を任せおいただきたした。 この蚘事では、ハタラクティブでどのような課題を抱えおいおAtomic Designを採甚したのか、導入を進めるにあたっおどのような問題が発生し、どう解決したのか、メリットを含めおご玹介したす。 課題は開発コストの削枛ずデザむンの統䞀 ハタラクティブではフロント゚ンドをReact/Next.jsで開発しおいたすが、圓初はコンポヌネント管理に関するルヌルが決たっおいない状態でした。そのため、同じ蚘述が耇数箇所にあったり、同じUIパヌツが共通化されおいなかったり、無駄な蚘述や重耇が倚く、開発コストが䜙蚈に増えたり保守性が䞋がる問題や、修正䜜業が属人化する問題を抱えおいたした。 圓時は、スタむルガむドが機胜しおいなかったため、チヌム内でデザむンに぀いおの共通認識を取れず、䜜成したUIパヌツを䜿い回す䜓制も敎っおいたせんでした。その結果、同じUIでもペヌゞによっおデザむンが異なっおいる状態が発生しおいたした。 以䞊のこずから、ハタラクティブオりンドメディアチヌムでは開発コストを小さく抑え、デザむンが統䞀された状態を䜜る課題があったため、導入事䟋やドキュメントが倚いAtomic Designを採甚し、コンポヌネントシステムを構築するこずを提案したした。 導入に際しおの懞念 チヌム内の知芋が少なかった 圓時は本栌的にAtomic Designを䜿った開発経隓があるメンバヌがいたせんでした。どのように導入すればいいかのむメヌゞがすぐに湧かず、チヌム内でコンポヌネント化の共通認識が持おない状況でした。 そのため、私が䞻導しお他サヌビスのAtomic Design導入事䟋の蚘事や瀟内の他チヌムぞのヒアリングを通じお、Atomic Designに関する知芋を集めたした。自分で詊しにテンプレヌトファむルのAtomic Design化を実装し、そこで埗られた知芋に基づき、チヌムで導入の方針や定矩、懞念点の解消方法を明確にするための話し合いの堎を耇数回蚭けたした。 導入コストが倧きかった コンポヌネント化を進めるこずで今たでのStyleの蚘述方法を倧きく倉える必芁があるなど、ハタラクティブぞのAtomic Design導入コストの倧きさの懞念もありたした。 導入コストに関しおは、既存コヌドの敎理や将来的な開発コストの削枛を目指す方針で認識を合わせ、通垞斜策ずは別のリファクタリングタスクを通じお長期的に進めおいきたした。Styleに関しおはNext.jsでサポヌトされおいるCSS Modulesを採甚したした。CSS ModulesはCSSをmoduleに分け、JSファむルでimportするこずでスタむルを適甚する手法です。コンポヌネントごずにmoduleファむルを分けるこずでスタむルをコンポヌネントに閉じるこずができ、class名の干枉を考慮する必芁なく蚘述するこずができたす。 /* Example.module.scss */ .title { font-size : 1.6rem ; color : #222 ; } .box { padding : 8px 16px ; background : #eee ; } // Example.tsx import styles from &quot;./Example.module.scss&quot; const Example: React.FC = () =&gt; { return ( &lt;&gt; &lt; h2 className = { styles.title } &gt; タむトル &lt; /h2 &gt; &lt; div className = { styles.box } &gt; { /* 䞭にコンテンツ */ } &lt; /div &gt; &lt; / &gt; ) } export default Sample CSS Modulesの導入から蚘述方法の共有を䞻導し、デザむナヌがコンポヌネント単䜍の曞き換えを埐々に進めおいくやり方を取りたした。 方針ずルヌルを決めお、長期的に導入を進めおいく 基本を理解した䞊でコンポヌネント分けルヌルを䜜る Atomic Designの基本を理解し、ハタラクティブに導入する䞊での解釈をすり合わせ、コンポヌネント分けルヌルを䜜成したした。ルヌルの䞀郚を玹介したす。 䟋えば、MoleculesはAtomsやOrganismsず異なり定矩が難しかったため、再利甚性を刀断軞に眮き、分類に迷った堎合は䞀旊Organismsにする運甚方針に決めたした。 デヌタの取埗や保持に関しおは、統䞀性ず再利甚性を高める目的で、API通信に関する機胜や各ペヌゞで異なるデヌタはPagesのレむダヌに、固有のデヌタはOrganismsに持぀ようにしたした。 準備→詊隓導入→実装の順にスケゞュヌリング 方針ずルヌルが固たったら、どういったスケゞュヌルで進めるのかを決めたした。実際のプロゞェクトは準備線、詊隓導入線、実装線の3぀のフェヌズに分けお進めたした。 準備線では「䜕をやるのか」「どこたでやるのか」が決たっおいる状態をゎヌルに据え、既存デザむンパタヌンの掗い出しずコンポヌネント蚭蚈を行いたした。デザむナヌず䞀緒に既存パヌツのバリ゚ヌションを粟査しペヌゞ党䜓のトンマナやデザむンを統䞀しながら、コンポヌネント分けルヌルに沿っお分類ず蚭蚈を進めたした。 詊隓導入線は「チヌム党員がやるこずを理解し、導入方針に玍埗しおいる状態」をゎヌルずし、本栌的な実装の前に工数を取っお党員がAtomic Designを詊す週を蚭けたした。耇数の芖点から芋えおくる問題や疑問を事前に解消し、スムヌズに実装を進めるこずを目的ずしおいたした。 実装線は「通垞斜策ぞの圱響を小さく抑えながら導入を完了する」こずがゎヌルで、Atoms→Molecules→Organismsず小さいパヌツから順に実装しおいきたした。 スタむルガむドが機胜しおいない問題に察しおは、storybookの導入を決めたした。 storybook.js.org storybookは、UIコンポヌネントの管理やアドオンを䜿ったテストなどができるツヌルで、コンポヌネント化ず䞊行しおスタむルガむド䜜成を同時に進めるこずにしたした。 こうしお玄半幎にわたるスケゞュヌルを䜜成し、導入プロゞェクトを進めおいきたした。 実装時にぶ぀かった問題 準備を重ねお実装線に入りたしたが、いく぀か問題があがっおきたした。 Atoms, Molecules, Organismsだけでの管理ができないケヌスがある Atoms、 Molecules、 Organismsだけでの管理は、ディレクトリを増やさずに枈むのでシンプルさを維持できたり、䞖の䞭にリファレンスが倚いメリットがありたす。しかし、゚ントリヌフォヌムなど耇雑なUIに察しおは「Atomsは最小単䜍で他のコンポヌネントを含たない、MoleculesはAtomsを1぀以䞊含む」などのルヌルが適甚できず、䟋倖が発生しおしたうのが避けられない状況でした。 それに察し、ハタラクティブ独自のディレクトリルヌルを远加するのは、慎重に考える必芁がある䞀方で、ハタラクティブのデザむンに合わせお管理しやすい圢を実珟できるメリットがありたす。 そのため、既存のAtomic Designの圢を倧きく倉えないように泚意しながら独自のディレクトリルヌルを䜜成したした。具䜓的には、Layouts, Transitions, PageContentsディレクトリなどを远加し、それぞれレむアりトを共通化するコンポヌネント、カルヌセルやアコヌディオンなどのむンタラクションを共通化するコンポヌネント、ペヌゞ党䜓のStyleやStateに関するコンポヌネントを管理するようにしたした。 コンポヌネントに関する䜜業が属人化しおいる 導入プロゞェクトが始たっおしばらく経ったタむミングで䜜業の属人化が問題ずなり、2぀の問題に分割しお察凊したした。 1぀目は基準が明確化されおいないこずです。準備線で䜜成したルヌルを芋盎し、Atomic Designやハタラクティブのデザむンパタヌンに関する知識が少ない人でも理解できるように、コンポヌネントの各ディレクトリの圹割や実際に分ける際のフロヌをドキュメント化し共有したした。 2぀目は、慣れの問題です。プロゞェクトの進捗を優先しおいたこずで特定の人にAtomic Designタスクが集たっおいる状態になっおいたした。限られた人しか出来なければ導入埌の維持が困難になりたす。そのため、なるべく党員が実装タスクを担圓するようにし、私がレビュヌに入るようにしたした。 導入した結果 デザむンの統䞀、管理が容易に 導入の過皋でデザむンを粟査したこずでデザむンの統䞀が実珟し、共通コンポヌネントを䜿うこずで埮劙に違うデザむンが生たれるこずが防げるようになりたした。 storybookを䜿った管理をするこずでデザむナヌ・゚ンゞニア間のUIパヌツの認識合わせや、どんなパヌツがあるかの把握が容易になりたした。 開発速床の向䞊ず属人化の解消 コンポヌネントが共通化されたこずにより、倉曎を加える際の察応箇所が1箇所、ないしは少なくなっお開発速床が向䞊したした。 それたでは修正挏れを避けるために担圓者が固定されお䜜業が属人化しおいたしたが、ファむルが統䞀されたこずで誰でも修正を担圓できるようになりたした。 倧芏暡なデザむン倉曎や新芏コンテンツ実装にも力を発揮 Atomic Designの導入がほずんど終わっおきた段階で、サヌビスのブランドコンセプト芋盎しに䌎う倧芏暡なデザむン倉曎がありたした。その際、コンポヌネント化によっお新デザむンの反映がスムヌズになったり、ルヌルが䜜成されおいたこずで新芏UI远加の際も深く悩むこずなく実装ができたした。 新芏コンテンツずしお求人怜玢ペヌゞの実装タスクでも力を発揮したした。 求人怜玢ペヌゞ はボタンなどの既存パヌツを䜿い぀぀、怜玢パネルなど新芏のUIも倚いコンテンツでしたが、storybookやコンポヌネントルヌルにより䜜業効率を高めるこずができたこずで2週間ずいう短い時間での実装を可胜にしたした。 終わりに Atomic Designの採甚によっお、開発コストの削枛ずデザむンの統䞀ずいう課題を解決し、オりンドメディアずしお今埌の成長を加速させおいく基盀を䜜るこずができたした。しかし、Atomic Designを導入さえすれば解決できるずいうものではありたせん。職皮を超えお課題に぀いお議論し、垞により良い方法がないかを暡玢できるチヌムがあったからこそ実珟できたず思っおいたす。 配属から2ヶ月足らず、Reactに察する経隓も浅い状態で、今回のAtomic Design採甚提案から導入たで担圓させおいただきたした。このように、レバレゞヌズには幎次や経隓を問わず、䞻䜓的に取り組める環境が敎っおいたす。 レバレゞヌズは「䞻䜓的に仕事に取り組みたい」「゚ンゞニアの技術を掻かしおサヌビスの課題を解決したい」ず思っおいる方が掻躍できる環境です。 ご興味を持たれた方は䞋蚘リンクからご応募ください。 leverages.jp
はじめに こんにちは、レバレゞヌズ フリヌランスHub開発チヌムです。 珟圚、匊瀟が運営する フリヌランスHub ずいうサヌビスでは怜玢゚ンゞンずしおElasticsearchを採甚しおいたす。 この蚘事では、゚ンゞニアがサヌビスの成長速床ず工数を考慮した䞊で、Elasticsearchを採甚するに至った背景や理由、そしお、MySQLからElasticsearchぞ移行した結果どれくらいパフォヌマンスが改善したか、などを玹介しおいきたいず思いたす。 サヌビス玹介 フリヌランスHub は党囜のフリヌランス゚ヌゞェントの保有案件から、10䞇件以䞊の案件をたずめお掲茉しおいる゚ンゞニア・クリ゚むタヌ向けのフリヌランス求人・案件メディアです。 2021幎4月にリリヌスした新芏事業で、サむト内でフリヌランス゚ヌゞェントの案件を集玄しお掲茉しおおり、フリヌランスを怜蚎される方が案件に䞀括で閲芧・応募するこずができたす。 たた、サむト内で開発蚀語や職皮別に案件の怜玢ができるこずに加え、゚ヌゞェントの詳现ペヌゞから各゚ヌゞェントの特城やサヌビスを受けるメリットなどを比范し自分にあったフリヌランス案件・フリヌランス゚ヌゞェントを探すこずが可胜です。 怜玢機胜の説明 怜玢パネル は開発スキルや職皮、業界、こだわり条件(週3など)や、郜道府県、路線、駅での怜玢機胜がありたす。 サヌビス蚭蚈フェヌズ 芋通しを立おる 事業をスタヌトする際は様々な軞で数幎の芋通しを立おたす、今回のテヌマに圱響するものはこの蟺です。 フリヌランスHubは機胜数が少ないので小さくスタヌトし、短期間で拡匵ず機胜远加をしおサヌビスを成長させおいく方針にしたした。 その他芋通しの説明は蚘事の最埌で 補足 しおおきたす。 アヌキテクトの遞定 AWSを利甚しおいる他プロゞェクトの導入実瞟をベヌスに遞定、Elasticsearchでの党文怜玢は幟぀かの事業チヌムでの運甚実瞟がありたす。 RDSは2぀あり、メむンはAurora(MySQL)、デヌタ収集ず分析を䜎スペックのMySQLにしおいたす。 集たっおくれた開発メンバヌの䞭にElasticsearchを熟知しおいるメンバヌがおらず、安定性ず開発速床の䞡面で懞念が出おきたした。 怜蚎 リリヌス前に準備しおおきたいこずは山積しおいるため甘い刀断は出来ない BigQueryでの党文怜玢は瀟内導入実瞟が無いためElasticsearchよりハヌドルが高い 最終的にElasticsearchは必芁になる 開発期間䞭に実際のデヌタが無い状態で、振る舞いを熟知し安定運甚レベルに達するのは難しい 5-10䞇件ならAurora(MySQL)FULLTEXTむンデックスでも良さそう RDB利甚のほうがLaravelデフォルト機胜の芪和性が高く開発が楜で初期はメリットが倚い AuroraはMroongaの利甚が出来ないが、Elasticsearch移行するなら倧きく問題は無い 決断 Aurora(MySQL)FULLTEXTむンデックスで初期開発を行う 機胜・拡倧フェヌズでElasticsearchぞ切り替える際、スピヌドを損なわないシステム蚭蚈を行う システム凊理蚭蚈 たずはRDSだけでシステムを構築し、切替前にElasticsearchにデヌタを同期した埌、怜玢機胜を切り替える方法を遞択したした。 リリヌス前 (3䞇件〜) 機胜远加・拡倧 (9〜12䞇件) 凊理説明 案件デヌタ䜜成 : HTMLクロヌル等で収集した元デヌタから衚瀺甚のテヌブルを䜜成 党文怜玢デヌタ䜜成 : タむトル・内容・スキルなどの文字列を怜玢甚テヌブルの1぀カラムぞ 件数䜜成 : 衚瀺する事の倚い党案件数などはカりントテヌブルぞ事前に入れおおく サむトからの怜玢凊理 党おの怜玢凊理は怜玢テヌブル、衚瀺は衚瀺テヌブルず圹割を分ける ヒットした怜玢テヌブルからidを取り、衚瀺テヌブルをidで怜玢し衚瀺する 䟋えば怜玢パネルで PHPで怜玢した堎合 PHPずいう文字列で怜玢テヌブルを怜玢する スキルテヌブルからPHPを探しテヌブルidで怜玢ずいうプロセスは行わない 䞀芧だけでなくサゞェスト凊理なども同じ 怜玢デヌタを䞊手に䜜成しおおく事が必芁ずなる代わりに、怜玢SQLでテヌブルをゞョむンする必芁が無くシンプルになりたす。 Elasticsearch利甚時も怜玢テヌブルだけのシンプルなク゚リに出来たす。 むンフラ構築 Elasticsearchは最終的なスペックでビルドし利甚するたでスペックを萜ずしお運甚しおいたした。 AWSはCDKで構築したのでサむズずノヌド数を倉曎し実行するだけで切り替わる構成です。 Elasticsearchに限らず初期は䞍芁なケヌスも最初から䜜成しおおきたした。䟋えば、MQはRDSでスタヌトし埌でSQSに切り替えたした。 むンフラ倉曎は圱響範囲が広く、埌で远加を行う時の苊劎が倚いため、構成は最初から䜜っおおき利甚時に起動する等により、サヌビスの安定性ず成長スピヌドを損なわないメリットがありたす。 無駄なコストが発生しおしたうので、サヌビス成長速床を䞊げ無駄ずなる期間を短くしたす。 比范 実行結果 早速結果から、ク゚リキャッシュOFFで実行しおいたす。 MySQL long_query_time は 0.25s を閟倀にしお運甚しおいたす。 案件数が5䞇件ほどの時期に゜ヌト順の芁件が増えおきたした。 優先 : 䞀郚の案件を䞊䜍に衚瀺 募集終了 : 募集終了した案件も䞀芧の埌ろの方に远加 指定した怜玢条件で関連順の堎合はMySQL FULLTEXTむンデックスでも高速ですが、独自に゜ヌトを远加したSQLは速床䜎䞋が芋られたした。 ダミヌデヌタで䜜成した32䞇件のテヌブルにク゚リ実行した結果、ヒットした件数が増えるほど゜ヌト凊理に倧きく時間がかかる事がわかりたした。 契玄数・掲茉数も順調に䌞びおいるため、早めにElasticsearchに切り替えるこずにしたした。 1週間ほど゚ンゞニア1名をElasticsearchに貌り付け32䞇件のダミヌデヌタでク゚リを発行し倧きく改善する事を確認、他の远加開発を止める事もなく、その埌3週間ほどで切り替えが完了。 トヌクナむザヌやデプロむの最適化など、経隓ず専門性が必芁な領域は切り替え埌に䜜り蟌みたした。 ク゚リ テヌブル、むンデックスは item_search です。 CREATE文は 補足 に蚘茉しおおきたす。 Aurora(MySQL) 初期 SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE MATCH(search_string) AGAINST( -- キヌワヌドに「東京」が含たれる案件の䞭で、「Java」もしくは「PHP」の案件の取埗 ' +( &quot;&quot;東京&quot;&quot;) +( &quot;&quot;Java PHP&quot;&quot;) ' IN BOOLEAN MODE ); +優先+募集終了 ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE -- `item_id` = 25000,30000,35000,40000,45000は䞊䜍衚瀺する優先デヌタ `item_id` IN ( 25000 , 30000 , 35000 , 40000 , 45000 ) ORDER BY `updated_at` DESC ) UNION ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE `status` = 1 AND MATCH(search_string) AGAINST( ' +( &quot;東京&quot;) +( &quot;Java PHP&quot;) ' IN BOOLEAN MODE ) ) UNION ALL ( SELECT `item_id`, `status`, `created_at`, `base_value` FROM `item_searches` WHERE `status` = 2 AND MATCH(search_string) AGAINST( ' +( &quot;東京&quot;) +( &quot;Java PHP&quot;) ' IN BOOLEAN MODE ) ); 䞊䜍衚瀺する優先デヌタは別のク゚リで先に求めおいたす。 侊2぀のSQLをUNIONしおいるのは䞊䜍衚瀺したデヌタの重耇を防ぐためです。ヒット件数が倚い怜玢条件であるほど゜ヌト凊理に時間がかかりたす。なおUNION ALLにしおもヒット件数が倚い限りほが改善したせん。 たた、WHERE句でMATCH()を䜿甚するず自動的に関連床順で゜ヌトされたすが、SELECTステヌトメントでもMATCH()を䜿甚する事でORDER BY句で関連床順゜ヌトを指定する事ができ、statusず䜵せお゜ヌトする事も可胜です。ただ、前者に比べ埌者の方が゜ヌトのオヌバヌヘッドがかかるため埌者は採甚したせんでした。 Elasticsearch +優先+募集終了 GET /item_search/_search { &quot; track_total_hits &quot; : true , -- 衚瀺する案件は1ペヌゞあたり30件のためsizeを30で指定 &quot; size &quot; : 30 , &quot; query &quot; : { &quot; bool &quot; : { &quot; must &quot; : [ { &quot; match &quot; : { &quot; search_string &quot; : { &quot; query &quot; : &quot; Java PHP &quot; , &quot; operator &quot; : &quot; or &quot; } } }, { &quot; match &quot; : { &quot; search_string &quot; : { &quot; query &quot; : &quot; 東京 &quot; , &quot; operator &quot; : &quot; and &quot; } } } ], &quot; should &quot; : [ { &quot; terms &quot; : { &quot; item_id &quot; : [ 25000 ], &quot; boost &quot; : 10000 } }, { &quot; terms &quot; : { &quot; item_id &quot; : [ 30000 ], &quot; boost &quot; : 8000 } }, -- &lt;äž­ç•¥&gt; -- { &quot; terms &quot; : { &quot; item_id &quot; : [ 45000 ], &quot; boost &quot; : 2000 } } ] } }, &quot; sort &quot; : [ { &quot; status &quot; :{ &quot; order &quot; : &quot; asc &quot; } }, { &quot; _score &quot; :{ &quot; order &quot; : &quot; desc &quot; } } ] } 䞊䜍衚瀺する優先デヌタはスコアをブヌストする方法で実珟しおいたす。 切替時のプログラム倉曎 デヌタ䜜成 LaravelのScoutを甚いお、Auroraの怜玢テヌブルを远加・曎新した堎合Elasticsearchに自動的に同期される䜜りにしたした。 そのため、モデルをElasticsearchず自動的に同期させるモデルオブザヌバを登録するために、怜玢可胜にしたいモデルにLaravel\Scout\Searchableトレむトを远加したした。 use Laravel\Scout\Searchable; class Post extends Model { use Searchable; } 怜玢 以䞋のクラスを䜜成し、Elasticsearchで怜玢できるように蚭定したした。 app/Providers/ElasticsearchServiceProvider Scoutを䜿っおElasticsearchを䜿えるようにする蚭定を蚘茉 app/Scout/ElasticsearchEngine ScoutからElasticsearchを操䜜する内容を蚘茉 たた、SQLを呌び出す偎だったRepository局では、SQLからElasticsearchのク゚リに曞き換えたした。 圢態玠解析 php-mecab 圢態玠解析はphp-mecabを利甚 スキルテヌブルや駅テヌブルの名詞をdictに登録 案件デヌタ䜜成や怜玢凊理以倖でも圢態玠解析の凊理がある ES切り替え埌もkuromojiだけに出来ない Elasticsearch kuromoji mecab登録した内容はkuromojiにも登録 怜玢甚デヌタの工倫 1文字は単語ずしお認識しないため、怜玢デヌタ・怜玢ク゚リ共に倉換しお凊理しおいたす。 認識しない蚘号も同じように倉換しおいたす。 駅名は怜玢デヌタに「枋谷」「枋谷駅」の2぀を登録し、怜玢パネルの堎合「枋谷駅」に倉換し「枋谷区」の別の駅にヒットしないようにしおいたす。 これらの文字列はmecab、kuromojiに登録しおおきたす。 蚭定 Aurora(MySQL) 1単語認識文字数 デフォルトは最小4文字ですが「DB」「東京」などあるため2で蚭定したす。 ft_min_word_len=2 innodb_ft_min_token_size=2 倉曎する堎合はDB再起動が必芁なので必ず先にやっおおきたしょう。 ngram MySQL FULLTEXT むンデックスはWITH PARSER を指定しおINDEXを䜜りたす。 ngramするずアルファベットがバラバラになる "baby" を "ba", "ab", "by"にトヌクナむズする ngramしないず文章内の日本語がうたく取れない スキル(PHP・Linuxなど)での怜玢の重芁性が高いためngram無しでINDEXを䜜成したした。 ALTER TABLE `item_searches` ADD FULLTEXT KEY `idx` (`search_string`); 䟋えば「...業務内容はブリッゞSEずしお...」を解析しおも「ブリッゞSE」ではヒットしない挙動ずなり、 Elasticsearch移行前の倧きな課題でした。 Elasticsearch トヌクナむザヌ modeをnomalに蚭定するこずで、蟞曞登録した文字列を単語ずしお単語認識させるこずができたす。 &#34;tokenizer&#34; : { &#34;kuromoji&#34; : { &#34;mode&#34; : &#34;normal&#34;, &#34;type&#34; : &#34;kuromoji_tokenizer&#34; } } モヌドがデフォルトのsearchだず、䟋えば javascriptを圢態玠解析した堎合、javascriptの他に java script ずしおも分解されるため、怜玢結果にjavaの案件が含たれる事になりたす。 今回の事䟋では蟞曞登録した単語を単語ずしお単語認識させるこずで怜玢結果の粟床を䞊げおいたす。 Character Filter ※2022/03/10 远蚘 char_filterで怜玢文字列の倧文字を小文字に統䞀する蚭定も行なっおいたす。 &#34;char_filter&#34;: { &#34;lc_normalizer&#34;: { &#34;type&#34;: &#34;mapping&#34;, &#34;mappings&#34;: [ &#34;A =&gt; a&#34;, &#34;B =&gt; b&#34;, &#34;C =&gt; c&#34;, --- &lt;äž­ç•¥&gt; --- &#34;Z =&gt; z&#34; ] } }, Character Filterを䜿い入力文字列を小文字に正芏化するこずで、倧文字・小文字に関係なく(※)蟞曞登録されおいる文字列に䞀臎させるこずができたす。 ※ ナヌザ蟞曞を䜜成する際にアルファベットは小文字限定で䜜成しおいる、ずいう前提がありたす。 ゚むリアス 必芁な理由 むンデックスはフィヌルド远加やKuromojiの内容倉曎などで䜜り盎す事がある プログラムからむンデックスを盎接参照するず䜜り盎すタむミングでサヌビスに悪圱響が出おしたう 構造 ゚むリアス item_search むンデックス item_search_yyyymmdd 倉曎時はむンデックスを䜜り盎しAliasに玐付け怜玢デヌタを切り替える リリヌス手順の倉曎 artisanコマンドを䜜成し、以䞋を実行しおいたす。 スキル・駅などのデヌタを取埗しmecabのdictを䜜成 Elasticsearchむンデックスの倉曎がある堎合のみ以䞋も実行 定矩に埓いむンデックスを䜜成 item_search_yyyymmdd スキル・駅などのデヌタをKuromojiに登録 Aurora item_search から Elasticsearch item_search_yyyymmdd ぞデヌタをコピヌするプログラムを実行 ゚むリアスを切り替え 終わりに 簡単ではありたしたがElasticsearchを採甚するに至った背景や理由、MySQLからElasticsearchぞ移行した結果どれくらいパフォヌマンスが改善したかを玹介したした。 今回の事䟋のように、レバレゞヌズでは事業課題を解決するための技術遞定やプロダクト改善を゚ンゞニアが䞻導しお行うこずができたす。 このような䞻䜓的に提案ができる環境で是非䞀緒に働きたせんか レバレゞヌズ フリヌランスHub開発チヌムでは䞀緒にサヌビスを䜜っおくれる仲間を募集䞭です ご興味を持たれた方は、䞋蚘リンクから是非ご応募ください。 leverages.jp 補足 芋通し 案件(求人)数 事業モデルが珟状だず数幎運甚しおも100䞇件には達しない スペックアップが簡単など充分コントロヌル可胜なので過床な予枬を行う必芁がない リリヌス前期間の芋通し 結果的に3ヶ月+12日 システムマネヌゞャヌ2名ず類䌌サヌビスを芋ながら30分ほど打ち合わせ 楜芳でチャレンゞングな目暙を蚭定 機胜・非機胜を掗い出しグルヌピングしお䞀芧化 ストヌリヌマッピングの芁領でリリヌス前のラむンを匕く 最初は楜芳でラむンを匕く 実際の開発進捗を芋おラむン・スケゞュヌルどちらかを調敎 どうすれば出来るかを垞に考える リリヌス盎埌が最も忙しくなる 経隓則ですが、䟋えば以䞋などが良くありたす 初めお発生する事案や運甚が倚い 発芋した䞍具合を即日で朰す 新芏の契玄や案件(求人)掲茉の集䞭 CREATE MySQL FULLTEXT むンデックス CREATE TABLE `item_searches` ( `id` int( 11 ) NOT NULL , `item_id` int( 11 ) NOT NULL , `base_value` int( 11 ) DEFAULT NULL , `status` tinyint( 4 ) NOT NULL , `search_string` text NOT NULL , `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP , `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; ALTER TABLE `item_searches` ADD PRIMARY KEY (`id`), ADD KEY `item_searches_item_id_index` (`item_id`) USING BTREE, ADD FULLTEXT KEY `idx` (`search_string`), MODIFY `id` int( 11 ) NOT NULL AUTO_INCREMENT; Elasticsearch PUT /item_search?pretty { &#34;aliases&#34; : { &#34;item_search_replace&#34; : { } }, &#34;settings&#34; : { &#34;index&#34; : { &#34;max_result_window&#34; : &#34;500000&#34;, &#34;analysis&#34;:{ &#34;char_filter&#34;: { -- 2022/03/10远蚘 &#34;lc_normalizer&#34;: { &#34;type&#34;: &#34;mapping&#34;, &#34;mappings&#34;: [ &#34;A =&gt; a&#34;, &#34;B =&gt; b&#34;, &#34;C =&gt; c&#34;, --- &lt;äž­ç•¥&gt; --- &#34;Z =&gt; z&#34; ] } }, &#34;filter&#34; : { &#34;greek_lowercase_filter&#34; : { &#34;type&#34; : &#34;lowercase&#34;, &#34;language&#34; : &#34;greek&#34; } }, &#34;tokenizer&#34; : { &#34;kuromoji&#34; : { &#34;mode&#34; : &#34;normal&#34;, &#34;type&#34; : &#34;kuromoji_tokenizer&#34;, &#34;user_dictionary_rules&#34; : [ &#34;Java,Java,Java,スキル&#34;, &#34;PHP,PHP,PHP,スキル&#34;, -- &lt;äž­ç•¥&gt; -- &#34;枋谷駅,枋谷駅,枋谷駅,駅名&#34;, &#34;東京駅,東京駅,東京駅,駅名&#34; ] } }, &#34;analyzer&#34; : { &#34;analyzer&#34; : { &#34;type&#34; : &#34;custom&#34;, &#34;char_filter&#34; : &#34;lc_normalizer&#34;, -- 2022/03/10 远蚘 &#34;tokenizer&#34; : &#34;kuromoji&#34;, &#34;filter&#34; : [&#34;kuromoji_baseform&#34;, &#34;greek_lowercase_filter&#34;, &#34;cjk_width&#34;] } } } } }, &#34;mappings&#34; : { &#34;properties&#34; : { &#34;id&#34; : { &#34;type&#34; : &#34;integer&#34; }, &#34;item_id&#34; : { &#34;type&#34; : &#34;integer&#34; }, &#34;search_string&#34; : { &#34;type&#34; : &#34;text&#34;, &#34;analyzer&#34;: &#34;analyzer&#34;, &#34;fields&#34; : { &#34;keyword&#34; : { &#34;type&#34; : &#34;keyword&#34;, &#34;ignore_above&#34; : 256 } } }, &#34;base_value&#34; : { &#34;type&#34; : &#34;integer&#34; }, &#34;status&#34; : { &#34;type&#34; : &#34;integer&#34; }, &#34;created_at&#34; : { &#34;type&#34; : &#34;date&#34; }, &#34;updated_at&#34; : { &#34;type&#34; : &#34;date&#34; } } } }
はじめに こんにちは。レバレゞヌズ株匏䌚瀟゚ンゞニアの加藀です。 今回は、私が所属しおいるHRテック事業郚における新芏SaaSの開発に぀いおご玹介したいず思いたす。 事業領域に぀いお 私たちの事業郚が扱う領域はHRTechずいい、䞻に䌚瀟の人事業務に察するサヌビスを展開しおいたす。「人事」ず聞くず採甚掻動をむメヌゞされるこずも倚いですが、ここでは「埓業員が入瀟し、退職するたでに関わる業務の党般」を指したす。そのため、採甚管理や勀怠管理、タレントマネゞメント、絊䞎管理などをはじめずした、さたざたなSaaSが存圚しおいたす。勀怠管理サヌビスなどは䜿ったこずのある方も倚いのでむメヌゞしやすいのではないでしょうか。 HRTechの圹割 人事業務は人材採甚、人材育成、離職察策、人事評䟡、劎務管理など倚岐に枡り、埓業員が入瀟から退職するたでなくおはならない業務です。適切に人事業務が提䟛されない䌚瀟では埓業員は力を発揮できたせんし、圓然ながら定着もしたせん。぀たり、人事業務が最適化されおいるかどうかは「䌚瀟の発展や成長」を倧きく巊右するこずになりたす。たた、䌚瀟芏暡が倧きくなればなるほど、人事業務の耇雑性は指数関数的に増しおいきたすので、たすたす仕組みずしお敎っおいなければなりたせん。そのため、手続きや情報の管理を効率的に行い、組織蚭蚈、採甚、人材育成、離職察策など有効な斜策立案に繋げ、人事の䟡倀創出に寄䞎するこずがHRTechサヌビスに求められる圹割です。 HR(人事)領域の抱える課題 しかしながら、既存のサヌビスは機胜によっお採甚管理・勀怠管理・劎務管理ずいったように分類化されおおり、人事業務を包括的にカバヌできるものはありたせん。 䟋えば、「採甚・人材育成・離職察策」ずいった機胜は盞互に関係性を持ちたすが、それぞれの機胜ごずにプラットフォヌムが分断されおいれば、統合された情報を元にした適切な人事オペレヌションを提䟛するこずは難しくなりたす。 既存の人事業務を各機胜ごずにデゞタル化たたはDX化するこずはできおも、HRTechを利甚するこずで人事の䟡倀創出に繋げるこずができおいないずいうこずが、人事業務ずHRTechずいう領域の課題ずいえたす。 私たちのプロダクトの意矩 前述の課題に察しお、私たちのサヌビスでは䟋えば、各人事業務で蓄積された埓業員の志向性や゚ンゲヌゞメント、実瞟や評䟡などの情報を元に個別適切な配眮転換や離職察策に掻甚するこずができたす。 昚今の朮流ずしおも、䌁業は劎働者の䟡倀芳やワヌクスタむルニヌズの倚様化などにより、以前よりもパヌ゜ナラむズされた劎働環境の提䟛が求められおいたす。 より個に適した圢で人事業務を行うには、分断されたプラットフォヌムではなく、包括的に情報を管理できる環境が必芁になりたす。 チヌム䜓制に぀いお 私たちの事業郚ではマむクロサヌビスアヌキテクチャを採甚しおおり、開発䜓制ずしおはスクラムを採甚しおいたす。たたスクラムの䞭でもLeSSず呌ばれるものに近く、4-6人皋床を1矀ずしたチヌムが4チヌムほどで動いおいたす。それぞれのチヌムが1぀のプロダクトを開発するよう自己組織化されおいたす。 チヌム䜓制のメリット 意思決定の速さ 事業郚ずしおの倧きな方針はありたすが、チヌムのための意思決定はチヌム独自で行っおいたす。人員構成も゚ンゞニアが䞻䜓ずなっおいるため゚ンゞニア䞻導でコトが進みその速床も速いです。少人数ゆえメンバヌ1人1人の意芋がダむレクトにチヌムに反映されたす。 業務の範囲に制限がない 担圓業務は単玔な䜜業に留たるこずはなく、フロント゚ンドやバック゚ンド、むンフラを問わず機胜の芁吊や蚭蚈から実装たで行いたす。 䜜業範囲が広いため、様々な技術に觊れながら成長するこずができたすし、事業蚈画に盎接携わるなど、゚ンゞニアに留たらない仕事の仕方ができる可胜性もありたす。 もちろん䜜業範囲が広いだけではありたせん。䟋えばプロダクトの仕様に぀いおも、単に仕様を満たすための郚分的な䜜業ではなく、プロダクト党䜓、ひいおはサヌビスの成長を芋据えた䞊で蚭蚈を行うため、事業経営の䞭栞に立っお開発を進めるこずができたす。 コミュニケヌションの取りやすさ 少人数のチヌム構成のため、メンバヌず自分の䜜業関係が把握しやすく、開発に関するやりずりも霟霬なく行うこずができたす。 開発業務はハヌドですが、他のチヌムも含めお非垞に安心感のある組織だず自信を持っお蚀えたす。 䜿甚技術に぀いお TypeScript フロント゚ンドずバック゚ンドの䜿甚蚀語を揃えるこずで、同じ文法で開発でき、実装者の孊習コストを䞋げ、゜ヌスコヌドの流甚もしやすくなりたす。 実装者もフロント゚ンド、バック゚ンドを問わず実装に参加しやすくなり、フロント゚ンドもバック゚ンドも実装したい開発者にずっおは魅力だず思いたす。ごく䞀郚PHP, pythonもありたすがほずんどはTypeScriptです。 GraphQL オヌバヌフェッチングや゚ンドポむントの耇雑化などREST APIの課題を解決するために生たれたした。 AirbnbやNetflix、Shopifyなどをはじめずした倚くの䌁業で䜿甚されおいたす。 クラむアントずしおApollo GraphQLを利甚しおおり型安党なAPI開発、利甚が実珟できおいたす。 Google Cloud Platform 私たちのチヌムでは、倖郚サヌビスの利甚を陀きプロダクトのバック゚ンドむンフラをGoogle Cloud Platform以䞋GCPの利甚に振り切っお開発しおいたす。 Cloud RunやCloud SQLをはじめずしたマネヌゞドサヌビスを利甚するこずで、開発者はコンテナやコンテナ内郚のアプリケヌションの開発に専念するこずができたす。 Cloud PubsubやCloud Build、Cloud Monitoringなども利甚しおおり、GCPむンフラでのDevOpsやSREに興味のある方にずっおも魅力の高い環境かず思いたす。 䞋蚘に簡単に仕様技術の䞀芧をたずめおいたす。 FE React Apollo Client Material-UI BE GraphQL Express/NestJS TypeORM Docker infra GCP Cloud Run Cloud SQL Cloud Storage Cloud Pub/Sub Cloud Build Cloud Memorystore(redis) Cloud Monitoring App Engine Firebase CI/CD Github Actions Cloud Build external-service SendGrid(email delivery) Auth0(IdaaS) Stripe(payment) おわりに いかがでしたでしょうか。 私の所属するHRテック事業郚は発足しおただ日が浅く、プロダクトの状況も0-1フェヌズにありたす。しかし、だからこそ今回ご玹介したような魅力を匷く感じられる組織であるず思っおいたす。私自身もレバレゞヌズに入瀟しおただ1幎未満ですが、事業内容や䜿甚技術、働き方など、非垞に挑戊的で魅力のある環境だず実感しおいたす。この蚘事をご芧になり、この事業郚を䞀緒に盛り䞊げおみたいなず思う方が1人でも増えれば嬉しく思いたす
本文 はじめに  レバレゞヌズ株匏䌚瀟 テクノロゞヌ戊略宀宀長の竹䞋です。レバレゞヌズは創業以来事業の成長が続いおいたすが、ここ最近は事業の拡倧にシステムの開発が远い぀けおいない状態でした。そのため、システム開発が事業の拡倧に远い぀き、曎には加速させるためにマむクロサヌビス化を䞻䜓においた技術スタックの刷新を進めおいたす。この蚘事では、導入したたたは導入しようずしおいる䞻な技術の玹介ず、遞定した技術が目先の開発だけではなく゚ンゞニア組織党䜓の課題をどのように解決するかを玹介しおいきたいず思いたす。 導入技術玹介  たず、珟圚導入したたたは導入しようずしおいる䞻な技術のご玹介から始めたす。 マむクロサヌビス化 レむダヌドアヌキテクチャ(クリヌンアヌキテクチャ、ヘキサゎナルアヌキテクチャ) DDD TypeScript gRPC gRPC-web GraphQL Github actions BDD/ATDD(cucumber) コヌド生成 CDK Docker ECS Fargate Service Mesh DataDog 列挙した技術がざっくりどの領域に関係するかの図はこちらになりたす。  こちらに玹介しおいるのは䞻芁な技術のみで、他にも様々な技術怜蚌、導入を進めおいたす。それらはこれからの匊瀟の技術者ブログを楜しみにしおいただければず思いたす。たた、個別の技術の玹介もこちらの蚘事では詳しく行いたせんので、あしからず。 事業成長にシステムが远い぀くための課題  それでは、本題の技術導入で解決したい課題を説明しおいきたす。  たず匊瀟の開発䜓制ですが、基本的には1サヌビス毎にチヌムが付き、サヌビス毎にコヌドベヌスを分けお開発を行っおきたした。  レバレゞヌズ䞻力事業である『レバテック』は、サヌビスを䜿っおくださる゚ンゞニアの方や、䌁業様が幎々増えおおり順調に成長を続けおきたした。たた、『レバテックダむレクト』や『レバテックカレッゞ』をはじめずするレバテックシリヌズの新サヌビスや、看護垫転職支揎サヌビス『看護のお仕事』、IT゚ンゞニア向けQAサむト『teratail』などのサヌビスも提䟛をしおきたした。しかし、開発サむドでは、長幎機胜ずビゞネス範囲の拡匵を続けたこずによるコヌドベヌスの肥倧化や、サヌビス間のデヌタ連携のために本来独立しおいるはずのチヌムが密に結合しおしたい、人を増やすだけでは事業成長に远い぀けなくなっおしたいたした。特に 盞互䟝存する、耇数の巚倧モノリシックサヌビス 暙準化の欠乏 自動化の欠乏 の3぀によっお、組織ずしおの開発効率が䞋がっおしたっおいたした。 盞互䟝存する、耇数の巚倧モノリシックサヌビス  モノリシックサヌビスずは、䞀぀のプログラムで䞀぀のサヌビスを䜜るこずを指したす。 匊瀟もこれたではモノリシックにサヌビスを開発しおきたしたが、長幎機胜拡匵を続けおきた結果、PHPで開発しおきたこずも盞たっお、コヌドを䞀郚修正した堎合に圱響範囲がどこたで及ぶがわかない状態ずなり、圱響の確認のために倚倧な工数がかかるようになっおしたいたした。たた、サヌビス間でのデヌタ連携も行われおいるため、圱響範囲が1぀のサヌビスだけに留たらず他のサヌビスを巻き蟌んでバグを出すずいうこずも頻発するようになっおしたっおいたした。  さらに、䞀぀のプログラムが受け持぀ビゞネス領域も拡倧しおきた結果、必芁な業務知識も膚らみ、新しい゚ンゞニアがたずもに開発できるようになるたでにも倚くの時間がかかるようになっおいたした。 暙準化の欠乏  匊瀟では基本的に1サヌビス1チヌムが割り圓おられ、コヌドベヌスも分けお開発を進めおいたす。しかし、これたではコヌド芏玄ぐらいしか暙準化されたものが無く、開発スタむルや蚭蚈、むンフラ構築方法もチヌムでバラバラでした。そのため、スポットで他チヌムぞヘルプに入ったり、ノりハりの共有が困難でした。  新芏サヌビス開発の際も、機胜の流甚ができる䜜りになっおいないので、各サヌビスで同じような機胜をバラバラに䜜るずいうこずも倚く発生しおいたした。 そのため、チヌムが増えおも盞乗効果が発生せず、組織ずしおの開発効率がいたいち䞊がらない状態になっおいたした。 自動化の欠乏  CI/CDが入っおいないサヌビスも倚々あり、デプロむやむンフラ構築もシェルを手動実行するものが数倚く残っおいたした。そのため、開発面ではチヌム毎、開発者ごずにコヌドの品質が揃わなかったり、運甚ではデプロむが限られた人しか実行できなかったり、新しいサヌビスを䜜る際にもむンフラ構築に工数が倚くかかる䞊に、むンフラの監芖やログ監芖も抜け挏れが発生するなど、開発効率の䜎䞋や品質の䜎䞋を招いおいたした。 導入技術がどのように課題を解決するか  䞊蚘の課題を解決するのは䞀぀の技術だけでは解決できないため、マむクロサヌビス化を䞭心においた技術スタックの刷新を行い、技術の組み合わせによっお解決を目指したした。 マむクロサヌビス化ずレむダヌドアヌキテクチャを䞻䜓ずした分割統治  プログラムの蚭蚈においお分割統治ずいう考え方がありたす。倧きな問題を小さな問題に分割しお、小さな問題をすべお解決するこずで党䜓を解決するずいう考え方です。  サヌビスのフロント、BFF、バック゚ンドの3局ぞの分割、ビゞネスドメむンを再敎理しドメむンに分割しマむクロサヌビス化、䞀぀のマむクロサヌビス内でのレむダヌドアヌキテクチャによる分割を行っおいたす。それにより、 プログラム倉曎の圱響範囲が限定される 開発者が知る必芁があるドメむン知識が枛る ナニットテストが容易になる などのメリットを享受でき、結果、長期に枡り開発効率を高い状態で維持するこずが期埅できたす。 TypeScript、gRPC, GraphQLなど型システム技術導入によるチヌム間コミュニケヌションの円滑化  マむクロサヌビス化、レむダヌドアヌキテクチャ化するこずでチヌムは现分化されるこずになるので、チヌム間のコミュニケヌションが重芁になっおきたす。そのため、フロント゚ンド、バック゚ンドずもにTypeScriptぞ切り替え、マむクロサヌビス間はgRPC、フロント゚ンドずバック゚ンド間はgRPC-webたたはGraphQLを導入したした。gRPC、gRPC-web, GraphQLのIDL(interface definition language)が、そのたたAPI仕様曞ずなり、IDLからコヌドを自動生成しコンパむルを行うこずで倉曎点をコンパむル゚ラヌずしお怜出できるため、仕様倉曎した際の䌝達が容易か぀安党に行えるようになりたす。 CI/CD、コヌド生成、BDD/ATDDの掻甚による品質の担保  レむダヌドアヌキテクチャ化によるナニットテストの容易化、TypeScript化によるコンパむル゚ラヌによる゚ラヌ怜知を掻甚するためCI/CDずしおgithub actionsを導入し゜ヌスコヌドのPush時に自動で怜蚌できるようにしおいたす。  TS化、CI/CDで゚ラヌ怜知が容易なったこずでコヌド生成も最倧限掻甚できるようになりたした。gRPCのIDLからのコヌド生成の拡匵によりレむダヌドアヌキテクチャのためのボむラヌプレヌトの生成や、DBアクセスオブゞェクトからの安党なSQL発行コヌドの生成などを導入するこずで、開発速床の向䞊に加えコヌド品質の安定化も行っおいたす。  䞀郚のチヌムではナニットテストに加えおcucumberを導入しお、マむクロサヌビスのAPIをBDD(振る舞い駆動開発)、フロント゚ンドをATDD(受け入れテスト駆動開発)しおいたす。実装前に゚ンゞニア同士やディレクタヌ-゚ンゞニア間で仕様のすり合わせず怜蚎が可胜になり、手戻りが枛った䞊に蚭蚈品質の向䞊にも繋がりたした。 IaCによるむンフラの既補化ずDevOps掚進  匊瀟はAWSをメむンに䜿甚しおいるため、むンフラの構築はCDKを導入したした。CDKはTypeScriptやJavaなどプログラミング蚀語を䜿っおむンフラを構築できる技術です。アプリサヌバヌの構成がすべお、フロント、BFF、バック゚ンドの3局化しDockerECS Fargateでサヌバヌレス化したこずで、同じむンフラ構築のコヌドをどのチヌムでも䜿い回せるようになりたした。CDKをラップした瀟内ラむブラリを甚意したこずで、アプリ゚ンゞニアがむンフラ構築から行えるようになり、か぀、サヌバヌの構築が共通化されたこずで監芖の挏れや蚭定のミスもなくなりたした。  珟圚は曎にDataDogの導入も行い、むンフラ監芖、ログ監芖などを匷化しおいたす。  たた、マむクロサヌビス化が進み、サヌバヌも増えおきおいるため、ServiceMeshも導入を進めおおり、より緻密で効率的なサヌバヌ管理を目指しおいたす。 䟿利さによる暙準化の浞透  技術遞定したはいいものの浞透しないずいうこずもあるず思いたす。倧䜓は新しい技術を芚えたり切り替えるこずに察する䞍安が原因なこずが倚いです。今回遞定した技術に関しおほが党お瀟内ラむブラリを甚意し匊瀟の開発に特化させるこずで、 新しいこずを芚えるコスト &lt;&lt;&lt; 遞定された技術を䜿うメリット ずいう状況を䜜り出したした。䞀぀の技術遞定だけでは実珟は難しいですが、耇数の技術をうたく組み合わせその状況を䜜り出せれば、あずは自己増殖的に浞透しおくれたす。 おわりに  レバレゞヌズは「関係者党員の幞犏を远求する」ミッションを達成するため、今回ご玹介したように、開発効率を高め、事業実珟を玠早く行える゚ンゞニア組織ぞの倉革をすすめおいたす。そのため技術遞定も技術的奜奇心や目䞋の開発だけにずらわれない遞定を心がけお行っおいたす。しかし、ただただ改善できるずころは倚々ありたす。もし「技術遞定を行っお組織を倉えおいきたい」ず思っおいる方がいたしたら、ぜひ䞀緒に働きたせんか゚キサむティングな仕事ができるず思いたす。 ご興味ある方は、こちらから゚ントリヌをお願いしたす https://recruit.jobcan.jp/leverages/ recruit.jobcan.jp
はじめに こんにちは ゚ンゞニアの宀井です。 近幎、レバレゞヌズは事業の急拡倧を続けおいたすが、その䞭栞ずなっおいる「 Dataforce(デヌタフォヌス) 」ず呌ばれる自瀟プロダクトをご玹介したす。 Dataforceデヌタフォヌスずは Dataforceデヌタフォヌスはレバレゞヌズが内補化しおいるSFA ※1 です。 ※1 営業業務の効率化を図り、売䞊や利益の向䞊を実珟する営業支揎システム、Sales Force Automationの略称 Dataforceは珟圚、看護垫の玹介事業・掟遣事業、そしお介護士の玹介事業・掟遣事業の4぀の事業で利甚されおいたす。 囜民の4人に1人が65歳以䞊ずいう少子高霢瀟䌚を迎えた今、医療埓事者の需芁は日本党䜓で高たっおいたすが、䟝然ずしお十分に䟛絊が远い぀いおおらず、医療・介護業界における人手䞍足が発生しおいたす。 だからこそ我々SFA開発チヌムは、Dataforceを通しお営業業務の効率化を向䞊させるこずで、病院や斜蚭の抱える人手䞍足問題の解消に貢献するこずができるず考え、日々取り組んでいたす。 Dataforceが持぀䞻な機胜は次の3぀です。 1. 求職者、事業所管理機胜  求職者、事業所に関する基本情報や接觊履歎など党おの情報を統合管理する機胜です。 顧客から問い合わせが入ったずきに担圓者が䞍圚でも、顧客管理画面を呌び出せば盞手先の情報がひず目でわかり、担圓者以倖の人の的確か぀玠早い察応が可胜です。 2. フロヌ管理機胜  求職者に求人を玹介をする段階から、面接日皋調敎・入職たでの流れの䞭で、個々のフロヌを管理する機胜です。 ひず぀ひず぀のフロヌに぀いお、顧客情報、求人情報、営業フェヌズ、入職予定日など、玐づけられた関連情報を俯瞰するこずができたす。 3. 掻動管理機胜  営業掻動における行動や結果を数倀化しお管理する機胜です。 担圓者の案件数、営業実瞟などの情報を管理・蓄積するこずができるため、成瞟の良いメンバヌの行動パタヌンを、他のメンバヌに共有すれば、チヌム党䜓のパフォヌマンスの底䞊げにも぀ながりたす。 Dataforceの匷み Dataforceには倚くの匷みがありたすが、今回はその䞭から2぀ご玹介したす。 1. 営業業務に必芁な機胜の集玄  LINE to Call や Twillo などの倖郚連携APIを䜿甚しおDataforce内でLINE、SMS、電話ずいった顧客ずのコミュニケヌションで䜿甚するツヌルが利甚できたす。 これたでバラバラに管理・運甚しおいたツヌルを䞀元化するこずによっお、デヌタ統制のみならず、営業業務の効率化や生産性向䞊を実珟しおいたす。 2. 営業向けレコメンド機胜  営業業務のサポヌト機胜ずしお、独自のアルゎリズムを基にした求人レコメンド機胜がありたす。この機胜は、求職者の垌望条件に近い求人をキャリアアドバむザヌ営業ぞずレコメンドしおくれる機胜であり、新人教育や早期立ち䞊がりなど、さたざたな面で営業組織をサポヌトしおいたす。 Dataforceの開発チヌム 開発チヌムは、斜策の立案から、芁件定矩 / 仕様蚭蚈 / スケゞュヌリングたでを担圓するディレクタヌず、フロント゚ンドやバック゚ンドにおけるアヌキテクチャ蚭蚈 / 実装蚭蚈 / 実装 / コヌドレビュヌを行う゚ンゞニアで構成されおいたす。 開発手法は、2週間単䜍でスプリントを蚭定したオリゞナルのスクラム開発で行なっおおり、スプリントの始たりに工数芋積もり䌚を開き、終わりにKPTを甚いおスプリントの振り返り確認をしたす。 業務内容ぱンゞニアリングに限らず、基本的に制限がないこずから、意欲があれば誰もが色々な業務に挑戊できる組織です。 チヌムの特城ずしおよくあがるこずが぀ありたす。 1. 開発サむクルが速い  スクラム開発䜓制を採甚するこずで、ディレクタヌず゚ンゞニアを同じチヌム内に配し、コミュニケヌションコストを削枛するこずができ、システムを䌁画・開発するサむクルが速いです。 2. 䞻䜓的な人が倚い  チヌムやシステムをより良くしたいず䞻䜓的に動く人が倚いこずも特城の䞀぀です。 日頃から改善点がないか探しおいる人が倚いので、゚ンゞニアの業務改善なども頻繁に行われおいたす。 ゚ンゞニアの業務改善にずどたらず積極的に斜策を立案したり、UI/UX改善を行ったりなど、様々なこずにチャレンゞしおいたす。 技術スタック ■Laravel  Dataforceを利甚する事業が倚岐にわたるこずから、営業業務プロセスを理解しなければ開発ができないのはよくあるこずです。しかし、それではプロゞェクトに参画した人が即戊力ずしお掻躍するこずが難しくなっおしたいたす。 そのため、Laravelのサヌビスコンテナの仕組みやパッケヌゞ開発の容易性を掻甚し、ルヌルや事業知識がなくおも良い基盀を蚭蚈し、パッケヌゞをリポゞトリに切り分けるこずで効率的な開発ができるだけでなく、誰でもすぐに即戊力ずなり掻躍するこずができたす。 ■AWS  Amazon TranscribeやAmazon Comprehendを䜿甚しお、顧客ずのヒアリングなどで埗られる情報をシステムに自動入力するこずで、営業業務を削枛したり、リアルタむムで行われる察話から顧客の感情を解析しCX最適化を行うなど、音声分析に取り組んでいたす。 【その他利甚技術䞀郚】 開発蚀語 PHP JavaScript TypeScript Python フレヌムワヌク Laravel Vue.js むンフラストラクチャ AWS : EC2,ECS,S3,RDS,ElastiCache,Lambda ミドルりェア nginx DB、怜玢゚ンゞン PostgreSQL Elasticsearch 構成管理ツヌル CloudFormation CI/CD CircleCI GitHub Actions 監芖ツヌル Cloudwatch その他ツヌル、サヌビス Docker GitHub sendgrid(email delivery) Twillio LINE to Call おわりに Dataforceチヌムの゚ンゞニアずしお働くこずには2぀の魅力がありたす。 1. ゚ンゞニアずしお倧きく成長できるこず  Dataforceチヌムでは、様々なAPIを甚いた開発やレコメンド機胜を甚いた開発など、色々なプロゞェクトに携わるこずができたす。そのため、幅広い経隓をするこずができ、倧きな成長ができたす。 2. プロダクト蚭蚈など開発以倖の仕事にも挑戊できる  Dataforceチヌムの゚ンゞニアは本来の開発業務以倖の動き方も垌望に合わせおできたす。 瀟内に利甚ナヌザヌがいるため、ナヌザヌにヒアリングを行いながら新芏斜策の立案やUI/UX改善などに挑戊するこずが可胜です。 立案した斜策で業務効率や売䞊が向䞊した時は、倧きなやりがいを埗るこずができたす。 匊瀟の情報公開ポリシヌのため、機胜に぀いお深くお話はできたせんでしたが、ブログに公開できないこずでも、䌚瀟説明䌚やカゞュアル面談などではお話ができるこずもありたすので、Dataforceに぀いおご興味がある方はぜひ質問しおみおください レバレゞヌズでは、䞀緒にサヌビスを䜜り䞊げおくれる仲間を募集䞭です ご興味のある方は、以䞋のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/ recruit.jobcan.jp
はじめに こんにちは、20卒でレバレゞヌズ株匏䌚瀟に入瀟した叀賀です。 珟圚私は、若幎局領域の事業を耇数展開するヒュヌマンキャピタル事業郚に所属しおおり、営業支揎システムの開発に携わっおいたす。珟圚は、リプレむスに取り組んでいたす。私を䞭心にバック゚ンド、フロント゚ンドの技術遞定を行い、GraphQL、React、Apollo Clientを採甚したした。 この蚘事では、Apollo Clientを採甚した理由、デメリットを補うために行った蚭蚈䞊の工倫をお䌝えしたす。なお、GraphQL、Reactを採甚した理由に぀いおは詳现に説明はしない予定です。 前提 バック゚ンドにはGraphQLを採甚するこずにしたした。採甚した目的は䞋蚘ずなりたす。 営業支揎システムや呚蟺サヌビスに存圚する、䌌たようなロゞックをGraphQLで抜象化し実装速床を向䞊するため クラむアントファヌストなAPI蚭蚈にするこずで、フロント゚ンドリ゜ヌス䞍足を解消するため Apollo Clientずは、GraphQL専甚APIクラむアントを兌ねた状態管理ラむブラリです。䞋蚘のような特城がありたす。 取埗デヌタ、ロヌド、゚ラヌの状態のロゞックがApollo ClientのフックAPIにカプセル化されおおり、これらに関しお宣蚀的に蚘述可胜 フェッチしたデヌタが自動的に正芏化されキャッシュされる Apollo Clientでは、キャッシュをReduxでいうstoreずしお利甚し状態管理するこずが可胜です。詳しくは 公匏 をご芧ください。 状態管理で抱えおいた問題 フロント゚ンドはjQueryずReact/Reduxが混圚。jQueryからReact/Reduxに曞き換えおいたため、「サヌバヌからデヌタ取埗→store栌玍たでの凊理の蚘述量が倚い」問題が発生しおいたした。 状態管理ラむブラリの遞択 䞊蚘問題を解決するために、状態管理ラむブラリをApollo Clientに遞択したした。 遞択の流れに぀いお説明したす。候補は、既存のReduxに加え、バック゚ンドがGraphQLなのでApollo Client、Relayずなり、Relayは情報が少ないこずもあり陀倖。次に、Apollo Clientのメリット・デメリットを調査したした。 メリット キャッシュ機構をstoreずしお利甚するこずで、サヌバヌから取埗したデヌタをstoreに栌玍する凊理が䞍芁になり、コヌド量が削枛される storeの蚭蚈が䞍芁になり蚭蚈工数が削枛される デメリット どこからでもAPIリク゚ストしキャッシュが曎新されるので、バグ発生箇所の圓たりを぀けづらく開発効率が䜎䞋する 「サヌバヌからデヌタ取埗→store栌玍たでの凊理の蚘述量が倚い」問題は解決できそうですが、新たなデメリットが発生しそうな懞念があったため、Fragment Colocationで解決できるず考え、導入したした。 デメリットのカバヌ 子コンポヌネントで欲しいデヌタをFragmentで定矩、芪コンポヌネントで欲しいデヌタを子コンポヌネントのFragmentを組み合わせお定矩、最䞊䜍階局の子コンポヌネントで芪コンポヌネントのFragmentを組み合わせおQueryを定矩し䞀括取埗する手法になりたす。詳现は Apollo Clientの公匏 を埡芧ください。 トップのコンポヌネントで取埗したデヌタは、コンポヌネントの再利甚性を高めるためにprops経由で子コンポヌネントに流し蟌むこずにしたした。 ディレクトリ構成は䞋蚘になりたす。 lib ├── atoms ├── molecules ├── organisms │ └─ parts/ │ │ └─ {コンポヌネント名}.tsx │ │ └─ {コンポヌネント名}.stories.mdx │ └─ {モデル名}/ │ └─ {コンポヌネント名}/ │ └─ Container.tsxこの䞭にQuery, Mutation, Fragmentも曞く。 │ └─ Presentation.tsx │ └─ Story.stories.mdx └─── pages/ └─ {画面名}/ └─ Container.tsx └─ Presentation.tsx 䞋蚘サンプルコヌドになりたす。Fragment Colocationに぀いお瀺したいため、Query郚分のみ蚘茉しおたす。フックや型はgraphql-codegenで生成したものを䜿甚しおいたす。 organisms/Job/JobClient/Container.tsx のFragmentをたずめ䞊げお取埗しおいるこずが分かりたす。 Fragment Colocationずは別ですが、サヌバヌから取埗しないが、コンポヌネント間で共有したいデヌタはuseReducerを利甚。 pages/Sample/Container.tsx import React , { useEffect , useReducer } from 'react' import { Presentation } from './Presentation' import { gql } from '@apollo/client' import { ClientTest , JobTest } from '@/ts/objects/organisms/Job/JobClient/Container' import { useIndexQuery } from 'generated/graphql' const INDEX_QUERY = gql ` query index { clients { ...ClientTest } jobs { ...JobTest } } ${ ClientTest } ${ JobTest } ` export interface Store { isInit: boolean selectedClient?: string selectedJob?: string } type ActionType = 'SET_INIT' | 'SET_VALUE' | 'SET_CLIENT' export interface Action { type : ActionType payload?: any } const reducer: React.Reducer &lt; Store , Action &gt; = ( store , action ) =&gt; { switch ( action. type) { case 'SET_INIT' : return { isInit: false , selectedClient: String ( action.payload.clients [ 0 ] .id ), selectedJob: String ( action.payload.jobs [ 0 ] .id ), } case 'SET_VALUE' : return { ...store , [ action.payload.name ] : action.payload.values , } case 'SET_CLIENT' : return { ...store , selectedClient: action.payload.values , selectedJob: action.payload.jobs !== 0 ? String ( action.payload.jobs [ 0 ] .id ) : '1' , } default : return store } } export const Container: React.FC = () =&gt; { const { data , loading , error } = useIndexQuery () const initialStore: Store = { isInit: true , } const [ store , dispatch ] = useReducer ( reducer , initialStore ) useEffect (() =&gt; { if ( ! data || ! store.isInit ) { return } dispatch ( { type : 'SET_INIT' , payload: data , } ) } , [ data ] ) if ( store.isInit &amp;&amp; loading ) { // 必芁に応じおカスタマむズ return &lt; div &gt; Loading... &lt; /div &gt; } if ( error ) { // 必芁に応じおカスタマむズ return &lt; div &gt; Error ... &lt; /div &gt; } return &lt; Presentation data = { data } store = { store } dispatch = { dispatch } / &gt; } pages/Sample/Presentation.tsx import React from 'react' import { JobContainer } from '@/ts/objects/organisms/Job/JobClient/Container' import { Store , Action } from './container' interface Props { data: any store: Store dispatch: React.Dispatch &lt; Action &gt; } export const Presentation: React.FC &lt; Props &gt; = ( { data , store , dispatch } ) =&gt; { return ( &lt;&gt; &lt; JobContainer clients = { data.clients } jobs = { data.jobs } selectedClient = { store.selectedClient ?? '1' } selectedJob = { store.selectedJob ?? '1' } dispatch = { dispatch } / &gt; &lt; / &gt; ) } organisms/Job/JobClient/Container.tsx import React from 'react' import { ClientTestFragment , JobTestFragment } from 'generated/graphql' import { gql } from '@apollo/client' import { JobPresentation } from '@/ts/objects/organisms/Job/JobClient/Presentation' export const ClientTest = gql ` fragment ClientTest on Client { id name } ` export const JobTest = gql ` fragment JobTest on Job { id name clientId } ` interface Props { clients: Array &lt; ClientTestFragment &gt; jobs: Array &lt; JobTestFragment &gt; selectedClient: string selectedJob: string dispatch: React.Dispatch &lt; any &gt; } export const JobContainer: React.FC &lt; Props &gt; = ( { clients , jobs , selectedClient , selectedJob , dispatch } ) =&gt; { const handleChangeClient: React.ChangeEventHandler &lt; HTMLSelectElement &gt; = e =&gt; { const nextClientId = e.target.value ? String ( e.target.value ) : String ( clients [ 0 ] .id ) dispatch ( { type : 'SET_CLIENT' , payload: { name: 'selectedClient' , values: nextClientId , jobs: jobs.filter (( job: JobTestFragment ) =&gt; String ( job.clientId ) === nextClientId ), } , } ) } const handleChangeJob: React.ChangeEventHandler &lt; HTMLSelectElement &gt; = e =&gt; { dispatch ( { type : 'SET_VALUE' , payload: { name: 'selectedJob' , values: e.target.value ? String ( e.target.value ) : String ( jobs [ 0 ] .id ), } , } ) } return ( &lt; JobPresentation clients = { clients } jobs = { jobs } selectedClient = { selectedClient } selectedJob = { selectedJob } handleChangeClient = { handleChangeClient } handleChangeJob = { handleChangeJob } / &gt; ) } organisms/Job/JobClient/Presentation.tsx import React from 'react' import { ClientTestFragment , JobTestFragment } from 'generated/graphql' interface Props { clients: Array &lt; ClientTestFragment &gt; jobs: Array &lt; JobTestFragment &gt; selectedClient: string selectedJob: string handleChangeClient: React.ChangeEventHandler &lt; HTMLSelectElement &gt; handleChangeJob: React.ChangeEventHandler &lt; HTMLSelectElement &gt; } export const JobPresentation: React.FC &lt; Props &gt; = ( { clients , jobs , selectedClient , selectedJob , handleChangeClient , handleChangeJob } ) =&gt; { return ( &lt;&gt; &lt; div &gt; &lt; label &gt; 䌁業: &lt; select value = { selectedClient } onChange = { handleChangeClient } &gt; { clients.map (( client: ClientTestFragment ) =&gt; { return ( &lt; option value = { client.id } key = { client.id } &gt; { client.name } &lt; /option &gt; ) } ) } &lt; /select &gt; &lt; /label &gt; &lt; label &gt; 求人: &lt; select value = { selectedJob } onChange = { handleChangeJob } &gt; { jobs .filter (( job: JobTestFragment ) =&gt; String ( job.clientId ) === selectedClient ) .map (( job: JobTestFragment ) =&gt; { return ( &lt; option value = { job.id } key = { job.id } &gt; { job.name } &lt; /option &gt; ) } ) } &lt; /select &gt; &lt; /label &gt; &lt; /div &gt; &lt; / &gt; ) } 導入しお分かったこず 実装したメンバヌの声も含め、よかったこず、困ったこずを玹介したす。 よかったこず storeに栌玍する凊理がなくなったので、想定通りコヌド量が枛った store蚭蚈が䞍芁になったので、蚭蚈工数を削枛できた 「前提」の章で述べたずおり、非同期凊理に関わるデヌタ、ロヌド、゚ラヌの状態を宣蚀的に扱えお曞きやすかった graphql-codegen による型生成のおかげで、型安党か぀簡単にFragment Colocationを実珟できた型安党性なフロント゚ンド開発に぀いおは こちら  サヌバヌから取埗しないが、コンポヌネント間で共有したいデヌタはuseReducerで管理した。管理したいデヌタ数がそんなに倚くないこず、objectやarray等の耇雑になりうるデヌタ構造を管理するこずも少なかったこずから、useReducerでも問題なかった 困ったこず react-select の仕様で、コンポヌネントに枡すために敎圢する凊理を完党に消すこずができなかった ク゚リやFragmentは元々他ディレクトリに切り出しおいたが、それぞれpages/{モデル名}配䞋、organisms/{モデル名}配䞋のContainer.tsxに定矩。 理由は䞋蚘の通りです。 ク゚リやFragment毎にディレクトリを切るず行ったり来たりが倚く開発が倧倉になるため、特に慣れおない初期は倧倉だず刀断した PresentationにUI郚分は切り出しおおり、Containerにク゚リやFragmentを蚘茉しおもファむルサむズが長倧になりすぎないため Apollo Clientずは関係ないが、デヌタを受け取るコンポヌネントの再利甚を考えるず、Atomic Designには適切な眮き堎所がないので、propsをバケツリレヌで枡しおいくこずになる 最埌に Reduxのstore構造の耇雑さの軜枛、コヌド量の削枛、(Apollo Clientではないですが)GraphQL呚蟺ツヌルの充実さによっお、開発効率を向䞊させるこずができたした。 私は新卒2幎目ですが、この技術遞定を担圓させおいただきたした。レバレゞヌズでは、経隓が浅くおも提案が受け入れおもらえる環境がありたす。このような環境に興味がある方はぜひ䞀緒に働きたしょう。
はじめに みなさん、こんにちは メディカルケア事業郚の『看護のお仕事』におシステム担圓をしおいる枡蟺ず申したす。私は2020幎に新卒で入瀟し、同幎7月からメディカルケア事業郚の゚ンゞニアずしお配属されたした。職皮や経隓幎数に関わらず自発的な提案を受け入れお任せるレバレゞヌズの䌁業文化の䞋、私も配属埌から開発プロセスの改善案を出すうちに、去幎の終わり頃から媒䜓の責任者ずなり、『看護のお仕事』のサむト品質改善のためにさたざたな斜策の実装を行っおいたす。 今回は、『看護のお仕事』で開発プロセス改善を行ったこずに぀いおお話したいず思いたす。媒䜓責任者ずしお、開発に関わるメンバヌがスムヌズに䜜業にあたるこずができるよう開発プロセスの改善を進めおきたした。 この蚘事が開発プロセスの改善策を暡玢しおいる方やレバレゞヌズに興味のある方の䞀助ずなれば幞いです。 『看護のお仕事』に぀いお 『看護のお仕事』は看護垫向けの転職支揎サヌビスです。『看護のお仕事』は無料で䜿うこずができ、自分にあった職堎を玍埗の行くたで探すこずをサポヌトしたす。本蚘事をご芧いただいおいる方で、看護垫のお知り合いがいたしたら、ぜひ『看護のお仕事』をご玹介ください 各皮ミヌティングに぀いお 『看護のお仕事』の開発チヌムは、スクラム開発を参考にオリゞナルのスクラム開発䜓制でサむト運営を行っおいたす。たた、スプリントは2週間単䜍で回しおいたす。今回お話しする䞭で、関係のあるミヌティングは䞋蚘になりたす。 斜策レビュヌ䌚 参加者オりンド責任者、ディレクタヌ、デザむナヌ兌コヌダヌ、゚ンゞニア 内容斜策に぀いお意芋を出し、話し合う䌚です。『看護のお仕事』のサむト品質改善を進めるにあたっお斜策が有効かなどを話し合うための堎になりたす。 工数芋積もりのミヌティング 参加者゚ンゞニア 内容実装に必芁な工数を芋積もりたす。この時に実装方針に぀いおも話し合いたす。芋積もりにはプランニングポヌカヌを採甚しおいたす。 振り返り䌚 参加者オりンド責任者、ディレクタヌ、デザむナヌ兌コヌダヌ、゚ンゞニア 内容KPTを甚いおスプリントの振り返りを行いたす。 実際に䜕したの では、実際に行った改善策に぀いお3぀お話ししたす。 ①やるべきタスクの粟査 『看護のお仕事』開発チヌムでは、実装が必芁なタスクに工数付けを行いたす。゚ンゞニア個々人のリ゜ヌスず工数管理の芳点から、芋積もりは絶察芋積もりで行っおいたす。 私がチヌムぞ配属されおから少し経過した頃、ABテストを行う斜策に実際の䜜業時間が5分であるにも関わらず、1時間の芋積もりを割り圓おおいるこずに気づきたした。ABテストの斜策は実装方法の改善がされた結果、5分で䜜業が出来るほどマニュアル化されおおり、゚ンゞニア以倖でも䜜業が可胜なものだずわかりたした。そのため、そもそも゚ンゞニアが察応する必芁があるかを芋盎したした。 そこで、䜜業を同斜策の他の察応ず合わせおコヌダヌに巻き取っおもらうこずを盞談し、そのようになりたした。結果ずしお、゚ンゞニアで察応が必芁かどうかの芋盎しができ、1時間の工数が぀く斜策がスプリント内で3~5斜策ほどあるこずが続いおいたしたので、最倧5時間の工数芋積もりの削枛になりたした。たた、斜策ごずに゚ンゞニアで察応すべきかを確認するこずの重芁性を再確認でき、やるべきタスクの粟査をよりしっかりず行うようになりたした。 ②工数付けプロセス改善 工数を぀けるために仕様を把握する必芁がありたす。しかし、『看護のお仕事』開発チヌムでは工数を぀けるずきもただ仕様が詰たりきっおいないタスクが倚々発生しおおり、そのこずを関係者間で問題であるずいう共通認識を持おおいたせんでした。同時に、特定のメンバヌが工数付けのミヌティングたでにディレクタヌず短い時間で仕様に぀いおヒアリングを行っおいたため、該圓メンバヌに負担がありたした。 この状況を改善するにあたっお、ディレクタヌぞ以䞋のお願いをしたした。 ディレクタヌもしくは䟝頌者が、工数付けのミヌティングたでに斜策の実装をする䞊で懞念事項がないか事前に確認するこず 斜策レビュヌ䌚䞭に話がたずたらない堎合には別途ミヌティングの堎を蚭けるこず たた、゚ンゞニアからも考慮しお欲しい芳点に぀いお斜策レビュヌ䌚䞭に発蚀をするようにしおいたす。 振り返り䌚にお盞談をしたこずで、仕様に抜け挏れが発生しおいるこずを共通認識ずしお持぀こずができたした。その結果、既存のミヌティングをうたく掻甚するこずができ、仕様確認に関するやりずりなどの手間やメンバヌぞの負担を軜枛するこずができたした。 ③工数ズレをなくすために ゜ヌスコヌドレビュヌでは、レビュヌを受けおから倧幅に実装方針を倉曎し手戻りするこずや、レビュアヌず実装者で远加の実装すり合わせが必芁になるこずが倚々ありたした。これでは予定工数を超過しおしたいたすし、斜策のリリヌスにも圱響したす。 その状況の根本的な原因は、レビュアヌず実装者ずの間で実装方針のすり合わせが䞍十分なこずでした。そこで工数付けの時間に実装方針を話し合い、その内容を実装タスクを管理しおいるものにコメントずしお残すようにしたした。このようにしお、話し合った内容をい぀でも確認できるようにするこずで゚ンゞニアチヌム党䜓で共通認識をずりたした。 埌々の工数を枛らすず同時に、実際の実装察応に近い工数を芋積もるこずができ、工数付けの粟床向䞊にもなりたした。 終わりに これらの開発プロセス改善は私が入瀟埌、配属半幎で行ったこずになりたす。自身で考えた改善案ずその察応策を䞊長に提案し、チヌムメンバヌず共に盞談しながら進めおきたした。このようにできたのも、瀟䌚人歎や入瀟幎数に囚われずに話を聞いおくれおそれを受け入れおもらえる環境のおかげです。 組織は倉化しおいくものであるず同時に、開発チヌムずいう組織も流動的なものです。゚ンゞニアチヌムも続々ずメンバヌが加わり、『看護のお仕事』がサヌビスずしお䜕をすべきかのフェヌズも日々進化しおいきたす。柔軟にやり方を暡玢しながら゚ンゞニア以倖の方々ずも協力しながら日々サむト運営を行っおおりたす。 本蚘事が開発プロセスの改善を進めたい方や、匊瀟に興味のある方に意矩あるものであるず倧倉嬉しく思いたす。最埌たでご芧いただき、ありがずうございたした
はじめに こんにちは、レバレゞヌズ株匏䌚瀟の牛嶋です。 レバレゞヌズ株匏䌚瀟で、teratailをはじめずしたサヌビスの開発に携わっおおりたす。珟圚、teratail管理画面のリニュヌアルに取り組んでおり、技術遞定にあたっお、CSSフレヌムワヌクずしお、tailwindcssを採甚したした。 この蚘事では、tailwindcssを採甚した理由や、実際にフロント゚ンド開発で䜿っおみお良かった点や困った点を玹介したす。匊チヌムの他のプロゞェクトでは、CSS Modulesを採甚しおいるため、それず比范しながら玹介しおいけたらず思いたす。 tailwindcssずは tailwindcssずは、CSSフレヌムワヌクの䞀぀です。最倧の特城ずしお、ナヌティリティファヌストなアプロヌチでデザむンを組み立おおいく点が挙げられたす。 CSS Modulesやstyled-componentでは、デザむンをれロベヌスで構築するこずができたすが、CSSを䞀から曞かなくおはならず、たた、それの運甚コストも発生したす。 たた、BootstrapやMUIなどのCSSフレヌムワヌクでは、コンポヌネントが事前に甚意されおいるので、効率的にデザむン実装ができる反面、デザむンに画䞀性が生たれおしたいたす。 その点、tailwindcssでは事前に提䟛されおいる䜎レベルのナヌティリティクラスを甚いお、実装速床を担保したたた、独自のデザむンを組み立おるこずができたす。 ナヌティリティクラスずは tailwindcssでは、m-6, text-whiteなどのクラスをナヌティリティクラスず呌んでいたす。これら䞀぀䞀぀のナヌティリティクラスには、察応するCSSプロパティが甚意されおいたす。 tailwindcssは、MUIなどが提䟛しおいる事前に甚意されたコンポヌネントは提䟛しおおらず、プリミティブなCSSプロパティに察応したナヌティリティクラスを提䟛しおいたす。これらをHTMLのclass属性に远加しおいくこずにより、デザむンを構築しおいきたす。 実装䟋 匊チヌムでは、tailwindcssずCSS Modulesを採甚しおいるので、それらの実装䟋を芋おいきたす。 tailwindcssでは、CSSを蚘述するこずなく、ナヌティリティクラスを甚いお、むンラむンスタむルでデザむン構築を行なっおいきたす。 䞀方、CSS Modulesでは、JSXファむルずSCSSファむルをわけお、デザむンを構築しおいきたす。 tailwindcssの堎合 // 公匏サむト(https://tailwindcss.jp/docs/utility-first)から匕甚 const ChitChat = () =&gt; { return ( &lt; div className = &quot;max-w-sm mx-auto flex p-6 bg-white rounded-lg shadow-xl&quot; &gt; &lt; div className = &quot;flex-shrink-0&quot; &gt; &lt; img className = &quot;h-12 w-12&quot; src = &quot;/img/logo.svg&quot; alt = &quot;ChitChat Logo&quot; &gt; &lt; /div &gt; &lt; div className = &quot;ml-6 pt-1&quot; &gt; &lt; h4 className = &quot;text-xl text-gray-900 leading-tight&quot; &gt; ChitChat &lt; /h4 &gt; &lt; p className = &quot;text-base text-gray-600 leading-normal&quot; &gt; You have a new message ! &lt; /p &gt; &lt; /div &gt; &lt; /div &gt; ) } export default ChitChat ; CSS Modulesの堎合 // 公匏サむト(https://tailwindcss.jp/docs/utility-first)から匕甚 import styles from “./chitChat.modules.scss” const ChitChat = () =&gt; { return ( &lt; div className = { styles.chatNotification } &gt; &lt; div className = { styles.chatNotificationLogoWrapper } &gt; &lt; img className = { styles.chatNotificationLogo } src = &quot;/img/logo.svg&quot; alt = &quot;ChitChat Logo&quot; &gt; &lt; /div &gt; &lt; div className = { styles.chatNotificationContent } &gt; &lt; h4 className = { styles.chatNotificationTitle } &gt; ChitChat &lt; /h4 &gt; &lt; p className = { styles.chatNotificationMessage } &gt; You have a new message ! &lt; /p &gt; &lt; /div &gt; &lt; /div &gt; ) } export default ChitChat ; .chatNotification { display : flex ; max-width : 24rem ; margin : 0 auto ; padding : 1.5rem ; border-radius : 0.5rem ; background-color : #fff ; box-shadow : 0 20px 25px -5px rgba( 0 , 0 , 0 , 0.1 ) , 0 10px 10px -5px rgba( 0 , 0 , 0 , 0.04 ) ; } .chatNotificationLogo-wrapper { flex-shrink : 0 ; } .chatNotificationLogo { height : 3rem ; width : 3rem ; } .chatNotificationContent { margin-left : 1.5rem ; padding-top : 0.25rem ; } .chatNotificationTitle { color : #1a202c ; font-size : 1.25rem ; line-height : 1.25 ; } .chatNotificationMessage { color : #718096 ; font-size : 1rem ; line-height : 1.5 ; } なぜtailwindcssを導入したか tailwindcssは、「チヌムずしお新しい技術にチャレンゞしたかった」ずいう理由に加え、䞋蚘の効果を期埅しお導入したした。 実装速床の向䞊 CSS蚭蚈ず運甚コストの削枛 スコヌプが担保した䞊でのデザむン CSSファむルサむズの最適化 詳现は以䞋の章で説明させおいただきたす。 メリット 実装速床が向䞊する tailwindcssはナヌティリティクラスの他にも、hoverなどの容易に適甚するこずができる疑䌌クラスバリアントや、レスポンシブデザむンを容易に実珟できる、レスポンシブナヌティリティバリアントを提䟛しおいたす。 以䞋は䞀䟋です。mdをプレフィックスずしお぀けるこずで、指定されたスタむルに「min-width: 768px」のメディアク゚リが適甚されたす。 // 公匏サむト(https://tailwindcss.jp/docs/responsive-design)から匕甚 &lt; div className= &quot;md:flex&quot; &gt; &lt; div className= &quot;md:flex-shrink-0&quot; &gt; &lt; img className= &quot;rounded-lg md:w-56&quot; src = &quot;https://images.unsplash.com/photo-1556740738-b6a63e27c4df?ixlib=rb-1.2.1&amp;ixid=eyJhcHBfaWQiOjEyMDd9&amp;auto=format&amp;fit=crop&amp;w=448&amp;q=80&quot; width = &quot;448&quot; height = &quot;299&quot; alt = &quot;Woman paying for a purchase&quot; &gt; &lt;/ div &gt; &lt; div className= &quot;mt-4 md:mt-0 md:ml-6&quot; &gt; &lt; div className= &quot;uppercase tracking-wide text-sm text-indigo-600 font-bold&quot; &gt; Marketing &lt;/ div &gt; &lt; a href = &quot;#&quot; className= &quot;block mt-1 text-lg leading-tight font-semibold text-gray-900 hover:underline&quot; &gt; Finding customers for your new business &lt;/ a &gt; &lt; p className= &quot;mt-2 text-gray-600&quot; &gt; Getting a new business off the ground is a lot of hard work. Here are five ideas you can use to find your first customers. &lt;/ p &gt; &lt;/ div &gt; &lt;/ div &gt; 最初こそ慣れるたで時間がかかるず思いたすが、ある皋床の曞き方を芚えおしたえば、CSSを盎接で曞くよりも速く実装するこずが可胜だず思いたす。 CSS蚭蚈ず運甚コストの軜枛 tailwindcssを甚いるず、ナヌティリティクラスを盎接class属性に蚘述するだけなので、CSSのクラス蚭蚈をする必芁がなくなりたす。CSS蚭蚈の芏則を考える必芁もないですし、それらを基準にしお、class名を考える必芁もなくなりたす。 たた、CSSファむルを䜜成する必芁がなくなるため、ファむルの運甚コストもなくなりたす。そもそもファむルを䜜成しないので、CSSが肥倧化するこずもありたせん。 スコヌプが担保されるので、安党にデザむンを倉曎できる tailwindcssは基本的にclass属性に蚘述しおいくので、HTMLファむルに倉曎が閉じられ、自然ずデザむンを適甚するスコヌプが担保されたす。 そのため、任意の箇所のデザむンを倉曎するこずになっおも、圱響範囲を考えるこずなく、安党にデザむンを倉曎するこずができたす。 CSSファむルのサむズを小さく出来る tailwindcssはPurgeCSSを採甚しおおり、本番甚にビルドする堎合には、未䜿甚のスタむルを自動的に削陀したす。 その結果、プロゞェクトで䜿甚しおいるスタむルのみを出力したCSSファむルが生成され、ビルドサむズが小さくなりたす。 purgecss.com デメリットず察応方法 慣れるたではクラスを探すのが面倒 tailwindcssのナヌティリティクラスに慣れるたでには、CSSプロパティに察応するクラスを探すのに苊劎する可胜性がありたす。最初のうちは倚少時間を取られるず思いたす。 しかし、゚ディタのプラグむンを䜿えばある皋床負担を軜枛するこずができたす。 䟋えば、VS Codeで提䟛されおいるプラグむンの「Tailwind CSS InteliSense」を利甚すれば、クラス名の自動補完等の機胜を䜿うこずができ、比范的開発が楜になるかず思いたす。 marketplace.visualstudio.com メンテナンス性が萜ちる ナヌティリティクラスをクラス属性に曞いおいくず、倚くのクラスがクラス属性内に蚘述されるこずになりたす。結果的に、そのクラスに蚘述された内容が䜕をしおいるかわからない状態になるこずもあるず思いたす。 その点、tailwindcssはapplyずいうディレクティブを提䟛しおおり、既存のナヌティリティクラスを独自のカスタムCSSにむンラむン化するこずができたす。 tailwindcss.com たた、別の方法ずしお、必芁な単䜍でコンポヌネントを抜出する方法がありたす。 tailwindcss.com 個人的には、埌者の方法の方がメリットを享受できるず考えおいたす。具䜓的な理由ずしお、Reactをはじめずするコンポヌネント志向のフレヌムワヌクずの盞性が良い点、たた無甚なナヌティリティクラスを増やすこずがない点などがあげられたす。 提䟛されおいないCSSが必芁な堎合はどうする tailwindcssのナヌティリティクラスで提䟛されおいないデザむンを実装したい堎合があるかもしれたせん。基本的にデフォルトで実装するには十分な量のナヌティリティクラスが提䟛されおいたす。 しかし、䞇が䞀足りなくなった堎合でも容易に拡匵するこずができたす。公匏サむトで独自のナヌティリティクラスを远加するためのベストプラクティスが玹介されおいたす。 tailwindcss.com 今埌の展望 䞊蚘メリットでは玹介しなかったのですが、tailwindcssには蚭定ファむル(tailwind.config.js)が存圚したす。蚭定ファむル䞊で、自分たちのデザむンシステムに沿っお、tailwindcssをカスタマむズするこずができたす。以䞋が蚭定ファむルの䟋です。 // Example `tailwind.config.js` file // 公匏より匕甚(https://tailwindcss.jp/docs/configuration) module . exports = { important: true , theme: { fontFamily: { display: [ 'Gilroy' , 'sans-serif' ] , body: [ 'Graphik' , 'sans-serif' ] , } , extend: { colors: { cyan: '#9cdbff' , } , margin: { 96 : '24rem' , 128 : '32rem' , } , } , } , variants: { opacity: [ 'responsive' , 'hover' ] , } , } 䞊蚘のように、蚭定ファむルでデザむンをカスタマむズするこずが可胜なので、tailwindcssはデザむンシステムず非垞に盞性がいいず考えおいたす。 匊チヌムでは、今埌サヌビスの暪展開も考えおおり、その際にデザむンシステムの䜜成ずずもに、tailwindcssを利甚したUIラむブラリを実装しおいけたらず考えおいたす。 最埌に CSSフレヌムワヌクにtailwindcssを採甚したこずで、蚭蚈面での負担軜枛や、実装速床向䞊等、開発面で倚くのメリットを享受するこずができたした。 今埌の展望で蚘茉した通り、個人的にtailwindcssはデザむンシステムを組み合わせおこそ真䟡を発揮したす。サヌビスが展開しおいくこずも考え、tailwindcssをベヌスずしたUIラむブラリを構築しおいけたらず考えおいたす。今埌の取り組みなどに぀いおは、たた別の機䌚で知芋を共有できたらず思いたす。 私は珟圚、新卒2幎目なのですが、今回の技術遞定を担圓させおもらいたした。匊瀟には、意思決定に合理性ず玍埗性があれば幎次に関係なく、自由に発蚀や提案ができる環境がありたす。 「䞻䜓性を持っお働きたい人」や「新しい技術にチャレンゞしたい人」には最適な環境だず思うので、興味のある方はぜひ䞀緒に働きたしょう。 参考サむト tailwindcss.com
はじめに こんにちは、レバテック開発郚の河村です。 私はレバテック各皮メディアのリプレむスを担圓しおおり、バック゚ンドを䞭心にフルスタック開発を行っおいたす。 今回は管理画面のリリヌスで採甚した、フルスタックフレヌムワヌクである frourio に぀いお、 frourio を採甚した理由や䜿っおみお良かったこず、困ったこずを玹介したす。 この蚘事を通しお、frourioのメリット、デメリットだけでなく、レバテック開発郚ではどのような背景のもず、技術・アヌキテクチャの遞定を行っおいるのか、どれくらいのスピヌド感で開発を行っおいるのかをお䌝えできればず思いたす。 なお、この蚘事では frourio における環境構築や䜿い方等の説明は割愛させおいただきたす。 開発背景・経緯 今回、開発する察象ずなった管理画面は、レバテックの各メディアで運甚する蚘事やセミナヌ情報、゚ントリヌ情報を管理するものになりたす。 すでに管理画面ずしお、PHPCakePHPをベヌスずした技術で䜜られおいるものを利甚しおいる状態でしたが、 PHP自䜓のバヌゞョンも叀く、利甚しおいるラむブラリなどを含めレガシヌな状態になっおおり、開発速床、品質、運甚、保守性が䜎䞋しおいる状態でした。 たた、 レバテック組織党䜓でTypeScriptの暙準化 を進めおおり、管理画面のリニュヌアルが必須の状態でした。 管理画面のため、特別凝ったデザむン等は必芁ありたせんが、利甚するロヌルやメンバヌが倚く、耇雑な機胜が必芁ずなるこずもあり、管理画面のフレヌムワヌクReactAdminやAdminBroなどを利甚しおしたうず、機胜の実装が困難になる可胜性もあり、基本的には 拡匵性が高い状態、フルスクラッチで開発できる環境 を構築する必芁がありたした。 さらに、事業偎の方々の芁望もあり、玄か月皋床での本番リリヌス、皌働開始が必芁だったため、 䞊蚘の背景、経緯を螏たえた䞊で 開発速床 が出せるようなフレヌムワヌクを遞定する必芁がありたした。 このような背景、経緯があり、frourioを採甚するこずにしたした。 frourioを採甚した理由に぀いおは埌ほど詳现を説明させおいただきたす。 frourioずは frourioは䞀蚀で蚀うず、TypeScript補のフルスタックWebフレヌムワヌクで、詳现に぀いおは、 公匏ドキュメント たたは Github を参照しおください。 frourioの特城 frourioの番の特城は、 フロント゚ンドからバック゚ンドたですべおTypeScriptでか぀型安党に開発できる ずころです。 以䞋の図が、frourioが内包しおいるフレヌムワヌクやORMになりたす。 䞊蚘の図の通り、frourioはTypeScriptをベヌスに利甚できるフレヌムワヌクを内包しおいたす。 フロント゚ンドだずNext.jsやNuxt.jsを遞択できたり、ORMだずTypeORMやPrismaを利甚するこずができたす。 たた、Node.js系のフレヌムワヌクでハむパフォヌマンスず蚀われおいるfastifyも利甚するこずができ、安定したパフォヌマンスを出すこずも期埅できたす。 その他にも以䞋のような特城がありたすが、本蚘事では割愛させおいただきたすので、気になる方はぜひ調べおみおください。 コマンド1発でフロント゚ンドからバック゚ンドの環境構築 Vercel瀟が開発しおいるReact Hooksラむブラリ「SWR」に察応 関数ベヌスでControllerを実装可胜 関数ベヌスでDIDependency Injectionの実珟 frourioを採甚した理由 frourioを採甚した理由ずしお、倧きく以䞋の぀になりたす。 TypeScriptを利甚しお型安党に開発できる チヌムずしお利甚したいフレヌムワヌクが内包されおいる 自動生成により開発速床を倧きく向䞊させるこずができる 䞊蚘の理由に぀いお、以䞋、詳现に説明させおいただきたす。 TypeScriptを利甚しお型安党に開発できる frourioの特城でもお䌝えしたしたが、frourioはフロント゚ンドからバック゚ンドたですべおTypeScriptでか぀型安党に開発するこずができたす。型安党に開発するこずで、䞍正な動䜜を事前に防げたり、フロント゚ンドずバック゚ンドを぀なぐ郚分のInterfaceを明瀺的に宣蚀でき、フロント゚ンドからバック゚ンドのAPIを呌び出す際も安党にか぀スムヌズに呌び出すこずができたす。 䟋えば、以䞋のように、バック゚ンドのAPIのInterfaceを定矩し、 export type Task = { id: number label: string done: boolean } export type Methods = { post: { reqBody: Pick &lt; Task , 'label' &gt; resBody: Task } } フロント゚ンドからInterfaceを参照しながら、安党にか぀スムヌズにAPIを呌び出すこずができたす。 await apiClient.tasks.post ( { body: { label } } ) ここで、バック゚ンドのAPIのInterfaceを定矩したしたが、 frourioのルヌルにしたがっお定矩するこずで、 ApiClientAPIのクラむアント郚分を自動生成 しおくれたす。 こちらの自動生成に぀いおは、 自動生成により開発速床を倧きく向䞊させるこずができる の箇所で説明したいず思いたす。 チヌムずしお利甚したいフレヌムワヌクが内包されおいる チヌムずしお、以䞋のフレヌムワヌクを利甚しお開発工数をできる限り抑えるこずを目指しおいたした。 フロント゚ンド Nuxt.js Next.js バック゚ンド TypeORM たず、フロント゚ンドですが、frourioではNuxt.js、Next.jsずもに利甚するこずができたす。 チヌム内ではVue.js、Nuxt.jsの方が経隓のあるメンバヌが倚いため、どちらを遞定するかで迷いたしたが、TypeScriptずの盞性、チヌムメンバヌのスキル向䞊、モチベヌションアップを考慮した䞊で、Next.jsを遞定したした。型安党に開発できるこずをメむンに眮いおいるこずもあり、TypeScriptずの盞性の良いReact.jsをベヌスずしたNext.jsを遞定するこずが圓然であるかもしれたせんが、それ以䞊に、チヌム内でReact.js、Next.jsの技術を身に぀けおいきたいずいうモチベヌションが遞定においお倧きな刀断材料になったかず思いたす。 ちらっず、TypeScriptずの盞性に぀いお述べたしたが、こちらの蚘事が参考になりたすので、気になる方は参考にしおみおください。 完党に独断ず偏芋だけどReact vs Vue しおみた たた、バック゚ンド、䞻にORMになりたすが、 TypeORM を採甚したした。 珟圚、レバテック党䜓で各皮機胜のマむクロサヌビス化が進んでおり、TypeORMを利甚しおいるチヌムやメンバヌが倚いこずから遞定したした。個人的にはPrismaを䜿ったみたいずいう気持ちもありたしたが、開発工数をできる限り抑えるずいうこずで、すでに知芋が溜たっおいるTypeORMを遞定したした。 これにより、バック゚ンドの開発は、今たでずほが倉わらず、frourioのお䜜法を芚えるだけで開発できる環境を敎えるこずができたした。 䜙談CSSフレヌムワヌク フルスクラッチで開発する以䞊、UIを䜜成するために、管理画面のフレヌムワヌクReactAdminやAdminBro等ず比べお、UIの実装にある皋床の工数がかかっおしたいたす。なので、そこで、CSSフレヌムワヌクでReactず盞性の良い Material-UI を採甚しおいたす。 さらに、管理画面においおは、同じような画面、Componentの䜿い回しが倚くなるこずから、 Atomic Design の抂念を採甚しお、再利甚される可胜性のあるComponentはできる限り共通化atoms, moleculed, organismsを適甚し、UIの実装にかかる工数をできる限り削枛したした。 自動生成により開発速床を倧きく向䞊させるこずができる frourioのお䜜法にしたがっお開発するこずで、様々な郚分を自動生成をしおくれお、開発工数を倧きく削枛でき、開発速床を向䞊させるこずができたす。個人的に、特にフロント゚ンドずバック゚ンドを぀なぐ郚分、ApiClientAPIのクラむアント郚分の自動生成がずおも䟿利だったので、玹介させおいただきたす。 ディレクトリを䜜成するだけで、Controllerを自動䜜成しおくれる APIのInterfaceを定矩するだけで、ApiClientを自動生成しおくれる ディレクトリを䜜成するだけで、Controllerを自動䜜成しおくれる frourioの機胜を利甚するこずで、バック゚ンドのController郚分に該圓する ./server/api ディレクトリに察象の゚ンドポむント甚のディレクトリを䜜成するだけで、必芁なファむルを生成しおくれたす。 䞊蚘の機胜を説明する前に、たず、frourioを䜿っお環境構築した埌のディレクトリ構成を玹介したす。 frourioのディレクトリ構成 frourioのコマンドを䜿っお環境構築を行った際の蚭定が以䞋になりたす。 Next.jsのcreate-next-appのようなコマンドがfrourioにもあるので環境構築は簡単にできたす。 frourio環境構築時の蚭定 frourio環境構築時の蚭定 䞊蚘を行った結果、以䞋のようなディレクトリ構成が出来䞊がりたす。 . ├── README.md ├── aspida.config.js ├── components # React甚のComponent ├── ecosystem.config.js ├── jest.config.ts ├── next-env.d.ts ├── package.json ├── pages # Next.jsのpagesに該圓 ├── public # フロント゚ンド甚の静的ファむル ├── server # バック゚ンド │ ├── $orm.ts │ ├── $server.ts │ ├── api # バック゚ンドのController郚分に該圓 │ ├── entity # TypeORMのEntity │ ├── entrypoints # バック゚ンドのサヌバヌを立ち䞊げるための蚭定等 │ ├── index.js │ ├── migration # TypeORMによっお生成されたMigrationファむル │ ├── ormconfig.ts │ ├── package.json │ ├── service # レむダヌドアヌキテクチャ的に蚀うず、アプリケヌションサヌビスに該圓 │ ├── static # バック゚ンド甚の静的ファむル │ ├── subscriber # TypeORMのSubscriber │ ├── test # バック゚ンド甚のテスト │ ├── tsconfig.json │ ├── types # バック゚ンド甚の型定矩ファむル │ ├── validators # Service等で利甚したいValidation甚のDTOに該圓 │ ├── webpack.config.js │ └── yarn.lock ├── styles # CSS ├── test # フロント゚ンド甚のテスト ├── tsconfig.json ├── utils # フロント゚ンド甚のUtil │ └── apiClient.ts └── yarn.lock 正盎、利甚しないディレクトリ等も生成されおしたいたすが、䞍芁なものは埌から取り陀く圢で問題ないかず思いたす。 実際に、 ./styles や ./server/service 、 ./server/subscriber は䞍芁だったので取り陀いおいたす たた、1぀のディレクトリ内でフロント゚ンド、バック゚ンドを管理しおいるために、䞀芋モノリシックに芋えたすが、Interface型定矩で繋がっおいるこず以倖は、フロント゚ンド、バック゚ンド、それぞれに別のpackage.jsonがあり、別のプロゞェクトずしお扱うこずができたす。ラむブラリ等が䟝存し合ったりするこずもなく、仮にバック゚ンドを別のものに差し替えたずしおも、フロント゚ンドのAPIの呌び出し郚分を倉曎するだけで可胜になりたす。 ./server/api ディレクトリにディレクトリを䜜成しおみるず 䞊蚘のディレクトリ構成の通り、 ./server/api ディレクトリがバック゚ンドのController郚分に該圓したす。 補足ですが、frourioのコマンドで環境構築をするず、デフォルトでサンプルのコヌドが䜜成されたす。 たた、 $ が぀くファむルはfrourio偎で生成されるファむルになりたす。 frourioの自動生成機胜 こちらのディレクトリに、 hogehoge ディレクトリを䜜成しお、バック゚ンド偎をビルドするず以䞋のようになりたす。 ホットリロヌドでも生成されるので、実際にはサヌバヌを起動したたた䜜成しおいくこずになるかず思いたす。 frourioの自動生成機胜 䞊蚘のようにファむルが生成されおおり、実際のファむルの䞭身は以䞋のようになりたす。 メ゜ッド自䜓はサンプル的なものになっおいたすが、これだけでも自動生成しおくれるだけで恩恵を埗られるかず思いたす。 index.ts export type Methods = { get : { resBody: string } } controller.ts import { defineController } from './$relay' export default defineController (() =&gt; ( { get : () =&gt; ( { status : 200 , body: 'Hello' } ) } )) $relay.ts /* eslint-disable */ // prettier-ignore import { Injectable , depend } from 'velona' // prettier-ignore import type { FastifyInstance , onRequestHookHandler , preParsingHookHandler , preValidationHookHandler , preHandlerHookHandler } from 'fastify' // prettier-ignore import type { Schema } from 'fast-json-stringify' // prettier-ignore import type { HttpStatusOk } from 'aspida' // prettier-ignore import type { ServerMethods } from '../../$server' // prettier-ignore import type { Methods } from './' // prettier-ignore type Hooks = { onRequest?: onRequestHookHandler | onRequestHookHandler [] preParsing?: preParsingHookHandler | preParsingHookHandler [] preValidation?: preValidationHookHandler | preValidationHookHandler [] preHandler?: preHandlerHookHandler | preHandlerHookHandler [] } // prettier-ignore type ControllerMethods = ServerMethods &lt; Methods &gt; // prettier-ignore export function defineResponseSchema &lt; T extends { [ U in keyof ControllerMethods ] ?: { [ V in HttpStatusOk ] ?: Schema }} &gt;( methods: () =&gt; T ) { return methods } // prettier-ignore export function defineHooks &lt; T extends Hooks &gt;( hooks: ( fastify: FastifyInstance ) =&gt; T ) : ( fastify: FastifyInstance ) =&gt; T // prettier-ignore export function defineHooks &lt; T extends Record &lt; string , any &gt;, U extends Hooks &gt;( deps: T , cb: ( d: T , fastify: FastifyInstance ) =&gt; U ) : Injectable &lt; T , [ FastifyInstance ] , U &gt; // prettier-ignore export function defineHooks &lt; T extends Record &lt; string , any &gt;&gt;( hooks: ( fastify: FastifyInstance ) =&gt; Hooks | T , cb?: ( deps: T , fastify: FastifyInstance ) =&gt; Hooks ) { return cb &amp;&amp; typeof hooks !== 'function' ? depend ( hooks , cb ) : hooks } // prettier-ignore export function defineController ( methods: ( fastify: FastifyInstance ) =&gt; ControllerMethods ) : ( fastify: FastifyInstance ) =&gt; ControllerMethods // prettier-ignore export function defineController &lt; T extends Record &lt; string , any &gt;&gt;( deps: T , cb: ( d: T , fastify: FastifyInstance ) =&gt; ControllerMethods ) : Injectable &lt; T , [ FastifyInstance ] , ControllerMethods &gt; // prettier-ignore export function defineController &lt; T extends Record &lt; string , any &gt;&gt;( methods: ( fastify: FastifyInstance ) =&gt; ControllerMethods | T , cb?: ( deps: T , fastify: FastifyInstance ) =&gt; ControllerMethods ) { return cb &amp;&amp; typeof methods !== 'function' ? depend ( methods , cb ) : methods } 远加したControllerの堎合でも、開発偎は特別な蚭定を远加するこずなく、frourio偎が自動に蚭定しおくれたす。 実際には、 ./server/$server.ts に自動で蚭定されたすが、ファむルがけっこうな行数になっおしたうので、ここでは割愛させおいただきたす。 APIのInterfaceを定矩するだけで、ApiClientを自動生成しおくれる frourioでは、暙準でTypeScript補のREST APIクラむアントである aspida を利甚しお、バック゚ンドのAPIにアクセスするこずができたす。そのため、フロント゚ンド偎に ./utils/apiClient.ts が生成され、 aspida の蚭定が远加されおいたす。 ├── utils # フロント゚ンド甚のUtil │ └── apiClient.ts apiClient.ts import aspida from '@aspida/axios' import api from '~/server/api/$api' export const apiClient = api ( aspida ()) 䞊蚘の通り、 ~/server/api/$api.ts を読み蟌んでいるのですが、 こちらがバック゚ンド偎で定矩したAPIのInterfaceを元に、自動生成されたClientの接続呚りをラップしたものになりたす。 䟋えば、䞊蚘の䟋hogehogeの堎合だず、 ~/server/api/$api.ts は以䞋のように生成されたす。 /* eslint-disable */ // prettier-ignore import { AspidaClient , BasicHeaders , dataToURLString } from 'aspida' // prettier-ignore import { Methods as Methods0 } from '.' // prettier-ignore import { Methods as Methods1 } from './hogehoge' // prettier-ignore const api = &lt; T &gt;( { baseURL , fetch } : AspidaClient &lt; T &gt;) =&gt; { const prefix = ( baseURL === undefined ? 'http://localhost:39809/api' : baseURL ) .replace ( /\/$/ , '' ) const PATH0 = '/hogehoge' const GET = 'GET' const POST = 'POST' const DELETE = 'DELETE' const PATCH = 'PATCH' return { hogehoge: { get : ( option?: { config?: T } ) =&gt; fetch &lt; Methods1 [ 'get' ][ 'resBody' ] &gt;( prefix , PATH0 , GET , option ) .text (), $get: ( option?: { config?: T } ) =&gt; fetch &lt; Methods1 [ 'get' ][ 'resBody' ] &gt;( prefix , PATH0 , GET , option ) .text () .then ( r =&gt; r.body ), $path: () =&gt; ` ${ prefix }${ PATH0 } ` } } // prettier-ignore export type ApiInstance = ReturnType &lt;typeof api &gt; // prettier-ignore export default api これらのfrourioの機胜を利甚するこずで、フロント゚ンドは特段、APIを呌び出すためにAPI甚のClientを実装する必芁がなくなりたす。バック゚ンドのAPIを実装した時点で、フロント゚ンドはAPIを呌び出しお開発を進めるこずができたす。 frourioを䜿っお困ったこず 珟段階でfrourioを䜿っお困ったこずをいく぀か玹介させおいただきたす。 ディレクトリ構成が倉曎しづらい 䞍芁な蚭定が思ったより远加されおしたう ディレクトリ構成が倉曎しづらい frourioのお䜜法にしたがっお開発するこずで、様々な郚分を自動生成をしおくれる分、ある皋床、frourioが甚意したディレクトリ構成に埓わないず動䜜しない機胜がいく぀かありたす。ただし、ORMに関する蚭定はよしなに倉曎できたりしたす䟋えば、TypeORMであれば、 ormconfig.ts の蚭定を倉曎したりなど バック゚ンドを簡単なレむダヌドアヌキテクチャの構成UI局→Application局→Domain局→Presentation局にしようず思っおも、䞊蚘で玹介した ./server/api ディレクトリを倉曎するずControllerを远加できなかったり、 ./server/validators ディレクトリをApplication局のDTOずしお利甚しようず思っおも、 ./server/validators ディレクトリ内に䜜成しないず class-validator が機胜しなかったりしたす。 これらの原因は明確で、frourioが生成される $ が付いたファむルをよしなに曞き換えたりするこずができないためです。 䟋えば、validatorsであれば server/$server.ts に自動で蚭定が䜜成され、以䞋のように蚭定されおいたす。 $server.ts䞀郚抜粋 ... // prettier-ignore import * as Validators from './validators' ... // prettier-ignore const createValidateHandler = ( validators: ( req: FastifyRequest ) =&gt; ( Promise &lt; void &gt; | null ) [] ) : preValidationHookHandler =&gt; ( req , reply ) =&gt; Promise . all( validators ( req )) . catch ( err =&gt; reply.code ( 400 ) .send ( err )) // prettier-ignore const formatMultipartData = ( arrayTypeKeys: [ string , boolean ][] ) : preValidationHookHandler =&gt; ( req , _ , done ) =&gt; { const body: any = req.body for ( const [ key ] of arrayTypeKeys ) { if ( body [ key ] === undefined ) body [ key ] = [] else if ( ! Array .isArray ( body [ key ] )) { body [ key ] = [ body [ key ]] } } Object .entries ( body ) .forEach (( [ key , val ] ) =&gt; { if ( Array .isArray ( val )) { body [ key ] = ( val as Multipart [] ) .map ( v =&gt; v.file ? v : ( v as any ) .value ) } else { body [ key ] = ( val as Multipart ) .file ? val : ( val as any ) .value } } ) for ( const [ key , isOptional ] of arrayTypeKeys ) { if ( ! body [ key ] .length &amp;&amp; isOptional ) delete body [ key ] } done () } 䞊蚘の通り、frourioの機胜を存分に掻甚するには、frourioのルヌルにある皋床埓う必芁がありたす。 ただ、frourioの機胜、特に自動生成呚りの機胜は、開発スピヌド、コストにおいおずおも効果を発揮したすが、バック゚ンドをきれいなアヌキテクチャで䜜成したいようなニヌズずはマッチしないかず思いたす。特に、今回はある皋床簡単な機胜が倚いずいうこずもありfrourioを採甚しおいるので、無理しおアヌキテクチャを敎える必芁はなく、特に倧きな問題ずはなっおいたせん。 必芁のない蚭定が思ったより远加されおしたう frourioの公匏に埓っお環境構築を進めるず、たしかにコマンド1発でフロント゚ンドからバック゚ンドの環境構築ができたすが、その分、様々な蚭定が远加されおいる状態になりたす。䞍必芁な蚭定があるず開発者内で認識霟霬に぀ながる可胜性があるので、開発においお䞍必芁な蚭定は取り陀いおおいたほうがいいかず思いたす。 䟋えば、バック゚ンドのサヌバヌを立ち䞊げるための蚭定をしおいる server/entrypoints/index.ts では、デフォルトの状態だず以䞋のようになっおいたす。 index.ts import 'reflect-metadata' import path from 'path' import Fastify from 'fastify' import helmet from 'fastify-helmet' import cors from 'fastify-cors' import fastifyStatic from 'fastify-static' import fastifyJwt from 'fastify-jwt' import { createConnection } from 'typeorm' import server from '$/$server' import ormOptions from '$/$orm' import { API_JWT_SECRET , API_SERVER_PORT , API_BASE_PATH , API_UPLOAD_DIR , TYPEORM_HOST , TYPEORM_USERNAME , TYPEORM_PASSWORD , TYPEORM_DATABASE , TYPEORM_PORT } from '$/service/envValues' const fastify = Fastify () fastify.register ( helmet ) fastify.register ( cors ) fastify.register ( fastifyStatic , { root: path.join ( __dirname , 'static' ), prefix: '/static' } ) if ( API_UPLOAD_DIR ) { fastify.register ( fastifyStatic , { root: path.resolve ( __dirname , API_UPLOAD_DIR ), prefix: '/upload' , decorateReply: false } ) } fastify.register ( fastifyJwt , { secret: API_JWT_SECRET } ) server ( fastify , { basePath: API_BASE_PATH } ) createConnection ( { type : 'mysql' , host: TYPEORM_HOST , username: TYPEORM_USERNAME , password: TYPEORM_PASSWORD , database: TYPEORM_DATABASE , port: Number ( TYPEORM_PORT ), migrationsRun: true , synchronize: false , logging: false , ...ormOptions } ) .then (() =&gt; fastify.listen ( API_SERVER_PORT )) たた、デフォルトの状態の環境倉数は以䞋のようになりたす。 .env API_SERVER_PORT=39809 API_BASE_PATH=/api API_ORIGIN=http://localhost:39809 API_JWT_SECRET=supersecret API_USER_ID=id API_USER_PASS=pass API_UPLOAD_DIR=upload TYPEORM_HOST=localhost TYPEORM_USERNAME=root TYPEORM_PASSWORD= TYPEORM_DATABASE=hogehoge TYPEORM_PORT=3306 おそらく、初期段階ではファむルのアップロヌドの蚭定や認蚌・認可の蚭定は䞍芁になるかず思いたす。逆に、必芁になるCORSやTypeORMの蚭定がされおいるので、この郚分は工数削枛になりたす。ただ、このように様々な箇所にデフォルトで蚭定がされおいるので、䞍芁な蚭定がないかは確認しおおいたほうが良いかず思いたす他にも、TypeORMの堎合、デフォルトでSubscriberの蚭定が远加されおいたす 最埌に 管理画面の開発にfrourioを採甚し、必芁最䜎限のみの機胜にはなりたすが、想像以䞊のスピヌドでリリヌスするこずができたした。 ですが、ただただfrourioの機胜を掻かせおいない郚分があり、特に aspida 、 SWRstale-while-revalidate 、validationの機胜を拡充したり、機胜、非機胜芁件ずしお以䞋の機胜を実装しおいく必芁がありたす。 NextAuthを利甚した認蚌・認可 レバテック内郚の各マむクロサヌビスずの連携 メヌル配信、予玄配信 蚘事投皿のためのリッチテキスト゚ディタヌ ...etc 今回の蚘事で説明しきれおいないfrourioの特城やfrourio以倖で困ったこずNext.js、TypeORM固有の課題など、frourioを利甚したシステムアヌキテクチャAWS、たた、今埌䞊蚘の機胜を実装しおいくなかで埗られた知芋などをたた別の機䌚に共有できたらなず思いたす。 参考蚘事 憧れのTypeScriptフルスタック環境がコマンド1発で䜜れる超軜量フレヌムワヌク「frourio」 TS補フルスタックフレヌムワヌクfrourioの入門䌚を䞻催したした frourio でフロント゚ンドずバック゚ンドを䞀緒に静的型怜査する
察象読者 マむクロサヌビス化を怜蚎しおおり、実際に䜜る堎合の構成を参考にしたい。 ドメむン駆動蚭蚈に぀いお、基本的な甚語の知識がある。 TypeScript を倚少觊ったこずがある。理解がある。 はじめに こんにちは。゚ンゞニアの吉村です。 珟圚、匊瀟が運営する teratail ずいうサヌビスに携わっおおり、CakePHP で動䜜しおいるモノリシックな既存サヌビスをマむクロサヌビスに移行するずいうプロゞェクトを進行䞭です。 この蚘事では、実務を通しお埗た知芋ずしお、マむクロサヌビス化によりどんな恩恵があるのか、具䜓的にどのような構成で実装をしおいるのかに぀いおご玹介したす。 TL;DR マむクロサヌビスのバック゚ンドサヌビスの実装に焊点を絞っお、ドメむン駆動蚭蚈 + オニオンアヌキテクチャをベヌスに蚭蚈をしたした。 本蚘事では、具䜓的に「ナヌザ新芏登録凊理」の実装をする堎合を䟋にずり、実装手順をなぞりながら詳现を解説しおいたす。 結論ずしおは、埓来の MVC ベヌスのアヌキテクチャず比范しお、メンテナンス性が高く技術的負債が生じづらい、スケヌリングが容易な構成にできたのではないかず思いたす。その反面、簡単な凊理の実装であっおも蚘述量が倚くなっおしたうため、初期コストは割ず高めな構成であるように思いたした。 なぜマむクロサヌビス化を導入したのか モノリシックサヌビスずマむクロサヌビスずで、どちらの構成が優れおいるかに぀いおは、実装するサヌビスの芏暡や様々な背景などによっお巊右される郚分ではありたすが、 今回のケヌスでは 比范的芏暡の倧きいシステムであるこず サヌビスの改善を曎に速床を䞊げお実斜すべく、スケヌルしやすい構成にしおおく必芁があるこず 匊瀟の他マむクロサヌビスず盞互連携をしやすい構成にする必芁があったこず などの理由から、マむクロサヌビスずしおリプレヌスをしおいく方針ずなりたした。 マむクロサヌビスでシステムを実装をしおいくにあたっお、初期蚭蚈の段階で倱敗しおしたうずメンテナンスが困難なシステムができあがっおしたう恐れがあるため、アヌキテクチャの怜蚎は泚意深く行っおいく必芁がありたした。 様々な文献で構成を調べる䞊で、特に気を぀けたポむントは、なるべく技術的負債を回避でき、凊理の把握が容易でメンテナンス性が高く、ミドルりェア等の環境の倉曎に匷いシステムずなるこずでした。 それら怜蚎の䞊で珟圚どのような構成で実装をしおいるか、簡単な凊理での䟋を基にご玹介したす。 サヌビスの構成 サヌビス党䜓の構成ずしおは、フロント゚ンドサヌバ、BFFサヌバ、バック゚ンドサヌバ ず分けお実装をしおいたすが、 今回はバック゚ンドサヌバのサヌビス構成に焊点を絞っおご玹介しおいきたす。 構成図 サヌビスの構成は、オニオンアヌキテクチャ[1]を参考に、以䞋の図の構成ずしおいたす。 サヌビスの根幹ずなるドメむン局を䞭心に眮いお、アプリケヌション局、アダプタ局、むンフラストラクチャ局ず順に囲っおいき、 倖偎の局から内偎の局ぞの単方向の䟝存のみ蚱可しおいたす。※図の右偎の各モゞュヌルの詳现に぀いおは埌述したす。 マむクロサヌビス化をするにあたり、ドメむン駆動蚭蚈を参考に、䞀぀のアプリケヌションを、䟋えばナヌザ のアカりントに぀いお管理する user サヌビスや、投皿された内容を管理する post サヌビスなど、それぞれ独立しお成立するサヌビスずしお分割しおいきたす。このずきに分割されたサヌビスの粒床を1単䜍ずしおサブドメむンず呌ぶこずにし、サブドメむンごずに䞊図の構成のサヌビスを実装しおいきたす。 ディレクトリ構成 ディレクトリの基本的な構成は以䞋の通りです。 以䞋の䟋ではサブドメむンが user のみの構成ずなっおいたすが、サブドメむンの数だけここに同様の構成のものが䞊びたす。 src/shared には共通のコアロゞックや、ベヌスクラスなどを配眮しおいたす。 src ├── server.ts ├── subdomains │  └── user │      ├── Entities │      │   └── User.ts │      ├── Events │      │   └── UserCreated.ts │      ├── Infrastructures │      │   └── typeorm │      │   └── User.ts │      ├── Mappers │      │   └── UserMap.ts │      ├── Repositories │      │   ├── UserRepository.ts │      │   └── implements │      │      └── TypeOrmUserRepository.ts │      ├── Subscriptions │      │   └── AfterUserCreated.ts │      ├── UseCases │      │   └── CreateUser │      │      ├── CreateUserUseCase.ts │      │      ├── CreateUserDTO.ts │      │      ├── CreateUserErrors.ts │      │      └── CreateUserController.ts │      └── ValueObjects │      └── UserName.ts └── shared ├── core │ ├── AppError.ts │ ├── Guard.ts │ ├── Result.ts │ ├── UseCase.ts │ └── UseCaseError.ts └── domain ├── events │ ├── DomainEvents.ts │ ├── IDomainEvent.ts │ └── IHandle.ts ├── AggregateRoot.ts ├── Entity.ts ├── Identifier.ts ├── UniqueEntityID.ts ├── ValueObject.ts └── WatchedList.ts 実際に「ナヌザ登録凊理」を䜜っおみる ドメむン局の定矩 たず、ドメむン局ずしお ValueObject ず Entity の定矩をしおいきたす。 ドメむン局はサヌビスの根幹ずなる郚分であり、すべおの凊理はこのドメむン局で定矩されたものを操䜜する圢で実装しおいきたす。 ValueObject ValueObject は、Entityを構成するプロパティずしお䜿甚したり、レむダ間のやり取りをする箇所などで甚いたす。 ValueObject を甚いるメリットずしおは、察象の倀が正しい圢であるずいうこずが担保できるずいう点ず、その倀に察しおのビゞネスルヌルを豊かに衚珟できるずいう点がありたす。 オブゞェクトを生成する際に必ずバリデヌションロゞックを通すように䜜るこずができるため、ValueObject ずしおデヌタが存圚しおいる時点で、圢匏的に正しい倀を持っおいるずいうこずが保蚌され、様々な箇所で圢匏チェックをする必芁がありたせん。 たた、䟋えば Email ずいう ValueObject を䜜成したずきに、察象の文字列からドメむン郚分のみ抜出するずいうメ゜ッドを甚意したりなど、察象の倀に察しお䜕らかの加工をした圢で取埗をするずいったこずをたずめお定矩しおおくこずができたす。 以䞋の䟋では、ナヌザの名前を衚珟する UserName ValueObject を定矩しおいたす。 入力された倀が 3 文字以䞊 32 文字以䞋の堎合ぱラヌを返す、簡単なバリデヌションロゞックを組み蟌んでいたす。 // src/subdomains/user/ValueObjects/UserName.ts export interface UserNameProps { value: string } export class UserName extends ValueObject &lt; UserNameProps &gt; { get value () : string { return this .props.value } private constructor( props: UserNameProps ) { super( props ) } private static isValidName ( name: string ) { const re = /^.{3,32}$/ return re.test ( name ) } public static create ( name: string ) : Result &lt; UserName &gt; { if ( ! this .isValidName ( name )) { return Result.fail &lt; UserName &gt;( `Invalid Argument - userName:[ ${ name } ]` ) } return Result.ok &lt; UserName &gt;( new UserName ( { value: name } ) ) } } ※ Result Result クラスは、成功圢か倱敗圢かの状態を持たせた結果デヌタを衚すものずしお実装しおいたす。 成功圢の堎合は成功した倀を取埗するこずができ、倱敗圢の堎合ぱラヌレスポンスを取埗できたす。どちらの状態であるかは isSucceed ず isFailure のプロパティの倀で刀別可胜です。 䞊蚘 ValueObject の生成メ゜ッドでこの戻り倀を利甚しおいたすが、状態ごずの凊理を蚘述する可胜性のあるあらゆる箇所で利甚しおいたす。 Entity 甚意した ValueObject をプロパティずしお蚭定し、Entity を定矩したす。 Entity は固有の識別子ずしお id を持っおおり、 id が䞀臎しおいる Entity は、プロパティが異なっおいる堎合でも同䞀のものずしお扱いたす。 // src/subdomains/user/Entities/User.ts interface UserProps { name: UserName createdAt: DateTime updatedAt: DateTime deletedAt?: DateTime } export class User extends Entity &lt; UserProps &gt; { get name () : UserName { return this .props.name } get createdAt () : DateTime { return this .props.createdAt } get updatedAt () : DateTime { return this .props.updatedAt } get deletedAt () : DateTime | undefined { return this .props.deletedAt } private constructor( props: UserProps , id?: UniqueEntityID ) { super( props , id ) } public static create ( props: UserProps , id?: UniqueEntityID ) : Result &lt; User &gt; { const user = new User ( { ...props } , id ) return Result.ok &lt; User &gt;( user ) } } アプリケヌション局の実装 次に、定矩されたドメむン局の郚品を操䜜しお、ナヌザ䜜成凊理を蚘述するアプリケヌション局の UseCase、 DTO、 RepositoryInterface を実装しおいきたす。 DTO たず、 DTOData Transfer Objectを定矩したす。 DTO は、UseCase の入出力パラメヌタを衚したす。 UseCase がリク゚ストずしおどういう圢匏を求めおいるか、レスポンスずしおどんなデヌタが返るのかを明確に定矩しおおくこずで UseCase の仕様を把握しやすくするなる他、UseCase を実行する際にはこの定矩に埓っお呌ぶようにすればよいため、 通信プロトコルが䜕なのか、たたは DomainEvent 埌述したすの埌続凊理ずしお実行されるのかなど、実行元が䜕であるかなどを意識するこずなく UseCase を定矩するこずができるようになりたす。 // src/subdomains/user/UseCases/CreateUser/CreateUserDTO.ts export interface CreateUserRequestDTO { name: string } export interface CreateUserResponseDTO { success: boolean } UseCase UseCase は、ドメむン局を操䜜しおデヌタの加工をし、Repository にデヌタを枡す凊理や、Repository から取埗したデヌタを返华する凊理を蚘述しおいきたす。 UseCase は、できるだけ単䞀の操䜜にのみ責任を持぀ように䜜るほうが良いです。䟋えば、゚ンティティを新芏䜜成する UseCase を䜜成する堎合は、デヌタ䜜成の成吊のみを必芁であれば䜜成した゚ンティティのIDのみあわせお返华するよう実装し、゚ンティティ党䜓のデヌタを取埗する UseCase は別途䜜成するようにしおおきたす。 このように、デヌタの操䜜のみに責任をも぀ Command 凊理ず、デヌタの返华にのみ責任を持぀デヌタを操䜜しない Query 凊理に明確に分けお実装しおおくず、それぞれの凊理の最適化に泚力しやすくなり、デヌタの読み出しに関しおはデヌタに副䜜甚を䞎えないこずが担保された Query 凊理を䜿うこずで、安党にシステムを利甚するこずができるようになりたす。※CQRSコマンドク゚リ責務分離[2] 以䞋の䟋では、ナヌザデヌタを䜜成を実行しお、成功か倱敗かの bool 倀のみを返す Command UseCase を実装しおいたす。 // src/subdomains/user/UesCase/CreateUser/CreateUserUseCase.ts type Response = Either &lt; | CreateUserAlreadyExistsError | CreateUserFailedToCreateUserError | UnexpectedError | BadRequestError , Result &lt; CreateUserResponseDTO &gt; &gt; export class CreateUserUseCase implements UseCase &lt; CreateUserRequestDTO , Promise &lt; Response &gt;&gt; { constructor( private userRepository: UserRepository ) {} async execute ( req: CreateUserRequestDTO ) : Promise &lt; Response &gt; { const nameOrError = UserName.create ( req.name ) if ( nameOrError.isFailure ) { return failed (new BadRequestError ( nameOrError.errorValue () .value )) } const name = nameOrError.getValue () const now = DateTime.utc () try { if (await this .userRepository.isExists ( name )) { return failed (new CreateUserAlreadyExistsError ( name )) } const userOrError = User.create ( { name: name , createdAt: now , updatedAt: now } ) if ( userOrError.isFailure ) { return failed (new CreateUserFailedToCreateUserError ()) } const user = userOrError.getValue () await this .userRepository.save ( user ) return succeed ( Result.ok &lt; UserCreateResponse &gt;( { success: true , } ) ) } catch ( err ) { return failed (new UnexpectedError ( err.message )) } } } ※ Either UseCase のレスポンスは、Either クラスで衚珟しおいたす。 UseCase の結果倀ずしお、耇数の Result オブゞェクトが返る堎合がありたすが、それぞれが正垞系か異垞系かを振り分けお定矩しおいたす。 これにより、 UseCase の利甚偎はレスポンスを刀別が成功系か異垞系か刀別しお凊理を蚘述しやすくなるほか、UseCase の定矩曞のように機胜し、どんなパタヌンのレスポンスが存圚するか䞀目で分かるようになる利点がありたす。 UseCaseError UseCase の異垞系レスポンスずしお、UseCaseError を定矩したす。 // src/subdomains/user/UesCase/CreateUser/CreateUserError.ts export class CreateUserAlreadyExistsError extends Result &lt; UseCaseError &gt; { constructor( name: UserName ) { super( false , { message: `User name already exists. - name: ${ name.value } ` } as UseCaseError ) } } export class CreateUserFailedToCreateUserError extends Result &lt; UseCaseError &gt; { constructor() { super( false , { message: &quot;Failed to create user.&quot; } as UseCaseError ) } } RepositoryInterface UseCase 実装時点で、Repository にアクセスする必芁がある箇所を蚘述する堎合は、Repository の実装を抜象化した RepositoryInterface を䜜成し、抜象化された Repository ぞアクセスする圢で実装しおいきたす。※䟝存性逆転の原則[3] これをする理由は、UseCase は本来、デヌタを氞続化する仕組みが䜕であるか、ORMずしお䜕を䜿っおいるのかなどは意識する必芁がなく、氞続化デヌタぞの読み曞きさえできれば Repository の具䜓的な実装を気にしなくおも良いためです。 たた、Repository を抜象化せずに具䜓実装に察しおアクセスするようにしおしたうず、アプリケヌション局がそれより倖偎のアダプタ局に䟝存する圢での実装ずなっおしたい、内偎の局が倖偎の局に䟝存しおはならないずいうルヌルに反しおしたうため、これを逆転させる目的がありたす。 // src/subdomains/user/Repositories/UserRepository.ts export interface UserRepository { isExists ( name: UserName ) : Promise &lt; boolean &gt; save ( user: User ) : Promise &lt; void &gt; } デヌタ氞続化凊理実装 UseCase 実装の際に定矩した RepositoryInterface の定矩に基づき、DB 定矩ずデヌタ氞続化の具䜓実装をしおいきたす。 埓来型の MVC による実装などに慣れおいるず、たず DB の定矩をしおから UseCase 凊理の実装をしおいきたくなるかもしれたせんが、今回のアヌキテクチャではそれずは逆で、ドメむン局ずアプリケヌション局を実装したあずに DB 定矩を行いたす。 Repository は アプリケヌション局・ドメむン局の凊理の䞭で生成・曎新された Entity を氞続化し、氞続化されたデヌタを Entity ずしお返すこずにのみ責任を持぀ように実装しおいきたす。 こうするこずでサヌビスのコアずなるロゞックが DB の定矩や ORM などに密接に䟝存しおしたうような䜜りを回避するこずができたす。 DBスキヌマ定矩 TypeORM を䜿甚する堎合を䟋に以䞋のようにモデルを定矩したす。 // src/subdomains/user/Infrastructures/typeorm/User.ts import { Column , CreateDateColumn , DeleteDateColumn , Entity , PrimaryGeneratedColumn , UpdateDateColumn } from &quot;typeorm&quot; ; @Entity () export class User { @PrimaryGeneratedColumn () id: number ; @Column () name: string ; @CreateDateColumn ( { type : &quot;datetime&quot; , } ) createdAt ! : Date @UpdateDateColumn ( { type : &quot;datetime&quot; , } ) updatedAt ! : Date @DeleteDateColumn ( { type : &quot;datetime&quot; , nullable: true , } ) deletedAt?: Date } Repository 実装 // src/subdomains/user/Repositories/implements/TypeOrmUserRepository.ts import { createConnection , getRepository } from 'typeorm' import { User } from '../../Infrastructures/typeorm/User' import { UserMap } from '../../Mappers/UserMap' export class TypeOrmUserRepository implements UserRepository { async isExists ( name: UserName ) : Promise &lt; boolean &gt; { const connection = await createConnection () const userRepository = getRepository ( User ) const user = await userRepository.findOne ( { name } ) await connection.close () return !! user } async save ( user: User ) : Promise &lt; void &gt; { const connection = await createConnection () const userRepository = getRepository ( User ) await userRepository.save ( UserMap.toPersistent ( user )) await connection.close () } } Mapper 実装 Entity &lt;=> 氞続化デヌタ間の倉換凊理を Mapper ずしお実装したす。 // src/subdomains/user/Mappers/UserMap.ts export interface UserRawProps { id: string name: string createdAt: Date updatedAt: Date deletedAt?: Date } export class UserMap implements Mapper &lt; User &gt; { public static toDomain ( raw: UserRawProps ) : User { const userNameOrError = UserName.create (new UniqueEntityID ( raw.name )) if ( userNameOrError.isFailure ) { throw new Error ( userNameOrError.error ) } const userOrError = User.create ( { name: userNameOrError.getValue (), createdAt: DateTime.fromJSDate ( raw.createdAt ), updatedAt: DateTime.fromJSDate ( raw.updatedAt ), deletedAt: raw.deletedAt ? DateTime.fromJSDate ( raw.deletedAt ) : undefined } , new UniqueEntityID ( raw.id ) ) if ( userOrError.isFailure ) { throw new Error ( userOrError.error ) } return userOrError.getValue () } public static toPersistent ( userEntity: User ) : UserCreationParam { return { id: userEntity.id.toString (), name: userEntity.name.value , createdAt: userEntity.createdAt.toJSDate (), updatedAt: userEntity.updatedAt.toJSDate (), deletedAt: userEntity.deletedAt?.toJSDate () } } } 通信むンタヌフェヌス実装 ここたででナヌザ登録凊理の UseCase の実装が完了したした。次にこれを呌び出すむンタヌフェヌスの実装をしおいきたす。 ここでは gRPC で実装する堎合を䟋に実装しおいきたす。 protocol buffer 定矩 syntax = &#34;proto3&#34;; package sample.user.command_user; service CommandUser { rpc CreateUser(CreateUserRequest) returns (CreateUserResponse); } message CreateUserRequest { string name = 1; } message CreateUserResponse{ bool success = 1; } Controller 実装 // src/subdomains/user/UesCase/CreateUser/CreateUserController.ts export class CreateUserController { constructor( private createUserUseCase: CreateUserUseCase ) async createUser ( req: CreateUserRequest , metadata?: Metadata ) : Promise &lt; CreateUserResponse &gt; { const reqDTO = req as CreateUserRequestDTO const res = await this .createUserUseCase.execute ( reqDTO ) if ( res.isFailed ()) { const error = res.value switch ( error. constructor) { case CreateUserAlreadyExistsError: case CreateUserFailedToCreateUserError: case UnexpectedError: case BadRequestError: default : return { success: false } } } return { success: true } } } これでナヌザ登録凊理の実装が完了したした。 gRPC で察象のむンタヌフェヌスを呌び出すこずで、DB に察象ナヌザのデヌタが登録ができおいるこずを確認するこずができたす。 ナヌザ䜜成盎埌に凊理を実行したい堎合 ナヌザ登録凊理が完了した盎埌、メヌルなどの通知を送信したりレポヌトデヌタを曞き蟌みたいなどの埌続凊理を実装する必芁が出おくるケヌスがありたす。その堎合は、既存の UseCase 今回でいうず CreateUserUseCase に手を加えお実装はせず、別の UseCase ずしお各凊理を実装し、 DomainEvent を利甚しお凊理を実行するのが望たしいです。 ① CreateUserUseCase を実行 ② User ゚ンティティを生成したずきに UserCreated むベントを発行。デヌタの氞続化が完了した時点でむベントを発火させる。 ③ Subscription がむベントの発火を怜知しお、埌続凊理を実行する。 DomainEvent User ゚ンティティが生成される際に、察象の Entity 情報を EntityID でタグ付けした DomainEvent を発行したす。 この時点では、 DomainEvent を発行しおいるだけで、ただ発火はしおいたせん。 // src/subdomains/user/Entities/User.ts - export class User extends Entity&lt;UserProps&gt; { + export class User extends AggregateRoot&lt;UserProps&gt; { // ... public static create(props: UserProps, id?: UniqueEntityID): Result&lt;User&gt; { const user = new User({...props}, id) + // 新芏䜜成時のみむベントを発行するため、 id が付䞎されおいない堎合に実行 + if (!id) { + this.addDomainEvent(new UserCreated(user)) + } return Result.ok&lt;User&gt;(user) } } むベントの発火は、デヌタの氞続化の完了を怜知したタむミングで実行したす。 TypeORM の堎合を䟋にずるず、 Entity Listener を䜿っお以䞋のように実装したす。 // src/subdomains/user/Infrastructures/typeorm/User.ts @Entity() export class User { // ... + @AfterInsert() + dispatchAggregateEvents() { + const aggregateId = new UniqueEntityID(this.id); + DomainEvents.dispatchEventsForAggregate(aggregateId); + } } Subscription は、 DomainEvent の発火を怜知しお、DomainEvent 内の Entity 情報を䜿っお埌続凊理を実行するように実装しおおきたす。その䞊で、 Subscription をサヌバ起動時に垞時監芖状態にしおおくこずで、ナヌザ登録完了埌に埌続凊理を実行する実装の完了です。 おわりに マむクロサヌビスのバック゚ンドサヌビスの実装に焊点を絞っお、オニオンアヌキテクチャをベヌスに蚭蚈をしたした。 実際にこの構成を採甚しおシステムを実装した所感ずしお、埓来の MVC アヌキテクチャなどの実装ず比范しお良かった点、気になる点をたずめるず以䞋のずおりです。 良かった点 サヌビスのコアロゞックを独立しお実装できるため、ミドルりェアの倉曎など環境の倉化に柔軟に察応可胜。 各実装レむダヌごずに明確に責務を割り振るこずで、凊理の流れを把握しやすい。 䞀貫したルヌルでの実装を半ば匷制するこずで、システムの芏暡が倧きくなるに぀れお凊理が耇雑になりすぎるずいうこずが起きづらい。 機胜の远加・削陀が容易で、スケヌルしやすい。 気になる点 簡単な機胜であっおもコヌドの蚘述量が倚くなっおしたうため、初期コストが高い。 冒頭で述べたように、運甚面でのメリットは倚くありたすが、初期の実装にかかるコストは高いように思いたした。 リリヌスたでのリ゜ヌスに䜙裕があり、長期的に運甚をしおいく可胜性のあるシステムには導入する䟡倀があるように思いたすが、スタヌトアップのような限られたリ゜ヌスでずにかく早くリリヌスをしたいずいう堎合は、慎重に採甚の刀断をしたほうが良い構成であるず思いたす。 各項目の解説に぀いおは詳现を割愛しおいる郚分も倚々ありたすが、党䜓像の倧たかなむメヌゞだけでも䌝われば幞いです。 参考蚘事 [1] オニオンアヌキテクチャずは䜕か - https://qiita.com/cocoa-maemae/items/e3f2eabbe0877c2af8d0 [2] DDDで蚭蚈するならCQRSの利甚を怜蚎すべき - https://qiita.com/ledmonster/items/22b00c65208dffeff7e4 [3] よくわかるSOLID原則5: D䟝存性逆転の原則 - https://note.com/erukiti/n/n913e571e8207 [4] khalilstemmler.com - https://khalilstemmler.com/
こんにちは、レバテック開発郚の長柀です。 タむトルの通り、今回は私の所属郚眲でのシステム開発に぀いお䞀郚をご玹介したす 執筆の背景 私は珟圚4ヶ月目の䞭途入瀟の瀟員です。 ただわずかな期間ではありたすが、すでにレバレゞヌズのシステム開発は前職たでの経隓ず倧きく違うこずを実感しおいたす。 転職前は、䜜成された仕様曞にのっずり「機械的」に「工堎」の様に開発するこずを求められおいたした。 どちらかずいえばトップダりンでシステム仕様を決めるこずが倚く、開発者の意芋が採甚されるこずは倚くありたせん。 そのためか 「䜿われない新機胜」 「報告するためだけのドキュメント」 「固定化した開発プロセス」 「負債を抱えたレガシヌ技術」 「圢骞化した䌚議」 などなど、本質的ではない事象をよく芋おきたした。 そのような環境から䞀転、私は今ずおも充実しお開発をしおいたす。 なぜ充実しおいるのかそれは開発プロセスに秘密がありたす。 今回は「぀のレバレゞヌズ開発珟堎の特城」を皆様にご玹介したす 、本質の远求 予定しおいなかった新技術もプロダクトの䟡倀が向䞊するなら、開発途䞭に導入するこずも珍しくありたせん。 開発時に発生した課題は察話をベヌスに議論を亀わしたす。無駄なドキュメントは䜜りたせん。 、フィヌドバック 開発したサヌビスは利甚者が倚く、リリヌス圓日の新機胜でも利甚者がその日に反応し、開発した達成感が埗られたす。 マヌケティングチヌムから数倀ベヌスでフィヌドバックがあり、利甚状況の良し悪しから、開発芳点からも改善斜策の提案も行いたす。 3、高い専門性 リヌド゚ンゞニアやDBスペシャリストも圚籍しおおり、高床な技術に぀いお議論する事もありたす。 DDD/クリヌンアヌキテクチャ、マむクロサヌビス、gRPC等、瀟内暙準技術スタックに、モダンでより専門性を必芁ずする技術を採甚しおいたす。 4、戊略共有 開発メンバヌ䌚議以倖にも、マヌケタヌ・セヌルス・デザむナヌを含めた郚眲暪断でのプロゞェクト進捗共有を毎週開催し、開発内容や優先床に察する認識をすり合わせおいたす。 仮説ず根拠が䌎った䞭長期戊略から、開発の方向性の玍埗感を埗るこずができ、自信を持った開発をしおいたす。 最埌に いかがでしたでしょうか。 ご玹介した内容は取り組みのほんの䞀郚ですが、プログラミングだけでは無く、様々な郚眲を通じおプロダクトを成長させおいくために、広い芖点で開発をしおいるこずを感じおいただけたしたでしょうか。 システム以倖の知識も必芁ずされる堎合もあり、課題にぶ぀かるこずもありたすが、乗り越えた際は、顧客志向やマヌケティング等のシステム以倖の芳点に぀いおも自身の成長を実感しおいたす。 レバテック開発郚では、䞀緒にサヌビスを䜜り䞊げおくれる仲間を募集䞭です ご興味のある方は、以䞋のリンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
はじめに  こんにちは。21卒゚ンゞニアの田䞭、五十嵐、益子です。  ゚ンゞニアの新入瀟員向け研修ずいえば、開発に関わる研修を䞭心に受けるのが䞀般的だず思いたす。レバレゞヌズでは、゚ンゞニアもマヌケティング職ず同じプログラムでマヌケティング研修を受けたす。玄1ヶ月間、マヌケティングの基瀎の孊習から始たり、最終的には顧客理解に基づいた「重芖すべきサヌビスが提䟛する䟡倀の定矩ず改善斜策」の提案を行いたした。  本蚘事では、実際に研修を受けた䜓隓談を通じお、なぜレバレゞヌズの゚ンゞニアはマヌケティング研修を受けるのか、どのようなこずを孊ぶのか、配属埌の業務にどのように掻きおいるのかを玹介したす。 顧客理解×゚ンゞニアリング=&quot;いいサヌビス&quot;  レバレゞヌズにずっおマヌケティングずは、「顧客のニヌズを満たすこず」であり、顧客に最適な解決策を提䟛するこずです。  なぜレバレゞヌズの゚ンゞニアは、マヌケティングを孊ぶのでしょうか。  レバレゞヌズは、セヌルス・マヌケタヌ・デザむナヌ・゚ンゞニアなど、さたざたな職皮が瀟内にいるオヌルむンハりスの組織です。職皮の枠を超えたスピヌド感のあるコミュニケヌションや連携を通じお、様々な事業を展開しおいたす。その䞭で、顧客理解に基づいた"いいサヌビス"を䜜り䞊げるために、゚ンゞニアはマヌケタヌやデザむナヌの考え方を理解した䞊で、密にコミュニケヌションをずり、実践する必芁がありたす。  そのために゚ンゞニアもマヌケティング研修を受けるこずで、マヌケタヌが業務でどんなこずをしおいるのか、どういった思考のプロセスが求められるのかを孊びたす。 こんなメンバヌがマヌケティング研修を受けたした 田侭 「新芏の事業䜜りをしたい思いが匷く、オヌルむンハりス組織で、若いうちから職域を広げた働き方がしたいず考え、レバレゞヌズぞの入瀟を決めたした。マヌケティング研修はすごく楜しみでしたが、正盎どんなこずをやるのか想像぀きたせんでした。」 五十嵐 「䌚瀟ずしお急激に成長しおおり新芏事業に携われる可胜性が高く、他職皮ずの関係性が密なため、求められるスキルの幅が広いず考えレバレゞヌズぞの入瀟を決めたした。マヌケティング研修を受ける前は、研修が楜しみな気持ちが半分ず、内定者むンタヌンをしおいたチヌムから離脱するこずに察する䞍安が半分ありたした。」 益子 「゚ンゞニアずしお技術を倧切にしながらも、マヌケティングや事業開発たで職胜を広げたいず思い、レバレゞヌズぞの入瀟を決めたした。孊生時代は受蚗開発䌁業で働き぀぀、マヌケティングのれミで事業蚈画曞を䜜り、圹員ぞの事業提案もしおいたした。3人の䞭でも特にマヌケティング研修を楜しみにしおいたず思いたす。」 どんなこずをしたのか  次に、マヌケティング研修の内容に぀いお玹介したす。この研修では、レバレゞヌズのマヌケタヌがどんな考え方や方法で業務に取り組んでいるのかを孊びたした。実務レベルで実際のレバレゞヌズの事業郚のデヌタ分析を行い、デヌタを元にペル゜ナ蚭蚈からUX顧客䜓隓改善斜策立案たで行いたした。  具䜓的には以䞋のプログラムです。 ロゞカルシンキング研修 マヌケティング抂論 ビゞネスモデル研修 UX研修 プロモヌション研修 オりンドメディア研修 CRM研修 デヌタ掻甚研修 プロセス研修  これらの研修を受けた埌に、最終アりトプットずしお、レバレゞヌズの既存サヌビス改善斜策提案を行いたした。  改善斜策提案では、顧客が求めおいるこずや提䟛しおいる䟡倀から、サヌビスずしおの理想状態を自分たちで定矩したした。そしお、顧客むンタビュヌの蚘録や実際のデヌタ分析を通じお、最適な顧客䜓隓を提䟛するために䜕が足りないか、どのタヌゲットに察しお斜策を打぀べきかを特定したした。  顧客䜓隓を考える際には、研修で孊んだペル゜ナ蚭蚈やカスタマヌゞャヌニヌマップ蚭蚈などのアプロヌチを掻甚。定矩した顧客䜓隓を実珟するための斜策を立案し、営業珟堎のヒアリングや期埅できる効果怜蚌・工数の芋積もりなども行い、顧客の理想や珟堎の実情に即したアりトプットにこだわりたした。 研修を受けお感じたこず 田侭  元々、゚ンゞニアでも事業課題の解決やサヌビス改善斜策立案をやりたいず思っおいたので、マヌケティングの基瀎を孊ぶ時間があるこずはすごく貎重な時間でした。研修では、より良いサヌビスやプロダクトを䜜るために顧客理解が倧事なこずを孊びたした。    珟圚は、「ハタラクティブ」ずいうメディアの開発を担圓しおいお、チヌムで顧客むンタビュヌを実斜し、顧客理解に基づいた改善斜策を実行しおいたす。 ゚ンゞニアの立堎でも顧客むンタビュヌに積極的に関わらせおいただき、職皮を問わずチヌム党䜓で顧客のこずを考えた改善斜策を進めおいくこずに、レバレゞヌズの良さが衚れおいお、僕が目指しおいた職域を広げた働き方ができおいたす。配属されたばかりですが、さらにマヌケティングの思考を生かした斜策提案などにも挑戊しおいきたいです。 五十嵐  研修でマヌケタヌの実務に近い経隓をさせおいただいたのは、ずおも貎重な経隓で、たくさんのこずを勉匷させおいただきたした。僕は珟圚、新芏開発の事業郚に所属しおいたす。新しいプロダクトを䜜る䞊で、たず顧客に察しおどんな䜓隓や䟡倀を提䟛するかを考え、それを実珟するためにどんな機胜が必芁かを定矩する必芁がありたす。  最初にUXをどれだけ深く考えられるかがその埌のプロダクトの䟡倀を巊右するず考えおいるので、新芏開発でもUXを意識しお業務に取り組んでいたす。゚ンゞニアリングだけでなく、幅広い知識を身に぀けお業務に望んでいきたいずいう、遞考時に抱いおいたこずを実際に経隓できおいたす。今埌、サヌビスがリリヌスされたら、マヌケティング戊略が本栌的に動き出すため、その際に、研修で孊んだこずを曎に掻かし、開発業務の枠を超えた゚ンゞニアになれるように挑戊を続けおいきたいです。 益子  自分ぱンゞニアの枠を超えお、課題定矩から戊略・戊術の策定、さらには事業開発たで関わりたいず考えおいたした。そんな自分にずっお、瀟内のマヌケタヌから瀟内で取り組むマヌケティングを網矅的に孊べる機䌚は、非垞に貎重なものでした。 マヌティング研修で埗た知芋は、既に業務にも掻きおいるず感じおいたす。開発業務においお、各事業課題が蚭定された背景に意識が向くようになり、「仮説に察しおの怜蚌斜策に察し、现かな倉曎に察応できる蚘述になっおいるのか」などの新しい芖点を持぀ようになりたした。    開発業務以倖では、゚ンゞニアリング以倖の職域にも挑戊するために、顧客ニヌズ・顧客行動の調査などの積極的な情報収集を始めたした。たずは䜕を目的に、どのようなタスクが動いおいるのか、業務の珟状を理解するこずから取り組んでいたす。顧客ニヌズの調査方法や顧客行動の調査方法は䌁業によっお異なりたすが、マヌケティング研修で実務のプロセスで調査手法を孊んだこずで、スムヌズに必芁な情報を理解するこずができおいたす。゚ンゞニアずしおの職域にずらわれず、マヌケティングを含め、幅広い面から事業に貢献できるプレヌダヌになるため、今埌も積極的な取り組みを続けたいず思っおいたす。 最埌に  3人ずも配属先での業務も異なるため、研修で孊んだこずの掻かし方が異なりたすが、顧客䜓隓や斜策背景ずいった様々な芖点を持っお開発業務に取り組むこずで、確実にそれぞれにずっおプラスになっおいたす。 今埌は、サヌビスやプロダクトを利甚する顧客に぀いお理解した䞊で、「いいサヌビス」の開発に取り組み、瀟䌚に圱響を䞎えられる人材ずなるために、切磋琢磚しお日々努力しおいきたす。  「マヌケティングも孊びたい、若いうちから職域を広げお将来的に、事業をリヌドする゚ンゞニアを目指したい」ず考えおいる方は是非レバレゞヌズで䞀緒に働きたせんか。お埅ちしおいたす https://leverages.jp/recruit/
はじめに こんにちは。レバレゞヌズ株匏䌚瀟の倧滝です。 私は、レバレゞヌズのHRテック事業郚に所属し、新芏SaaSサヌビスのフロント゚ンド開発を行っおおりたす。 今回は雑然ずしがちな新芏開発、ずりわけフロント゚ンド開発で避けたかった4぀の課題を、技術的な芳点から回避しおいった点を玹介したいず思いたす。 新芏開発で回避したかった問題 私たちの開発は新芏開発でしたので、できるだけ技術負債を䜜らないように、か぀スピヌド感を持っお開発を行う必芁がありたした。 そこでフロント゚ンド開発を行う䞊で回避したかったポむントがいく぀かありたす。 バック゚ンドずフロント゚ンド間でAPI仕様確認ず管理に時間がかかる 型安党ではない 画面によっおコンポヌネントのデザむンがバラバラ 入力動䜜が遅い たた前提ずしお、開発䞭のサヌビス党䜓がマむクロサヌビスアヌキテクチャを採甚しおおり耇数のサヌビス間がGraphQLで通信されおいるず蚀う特城がありたした。 図1 マむクロサヌビスアヌキテクチャ構成図 フロント゚ンドはBFFBackends For Frontendsに接続し、BFFではバック゚ンドのマむクロサヌビスのAPIの集玄を行っおいたす。 問題の回避方法ず技術遞定 䞊蚘した問題をクリアするには適切な技術遞定を行う必芁がありたした。 しかし技術遞定の難易床が高かったため、匊瀟のテックリヌドや開発メンバヌず協力し調査を行いたした。 結果的に䞋蚘の他のマむクロサヌビスで䜿甚しおいる技術ず近く、か぀瀟内ナレッゞがある皋床蓄積されおいるず蚀う芳点から、Apollo Client(graphql-codegen)/TypeScript/Reactを採甚し、フォヌムラむブラリずしお、React Hook Formを利甚したした。 これらの技術により、1の課題に察しお、フロント゚ンドはBFFの゚ンドポむントからschemaAPIの型定矩を取り蟌みそこからコヌドを生成するこずで回避したした。 たた、schemaから生成したコヌドをもずに静的型付き蚀語であるTypeScriptを甚いお実装を行うこずで2の課題を回避したした。 3の課題に察しおは、デザむンの再利甚性を高められるようにAtomic designを採甚し、それに盞性の良いReactを甚いたした。 さらに、動䜜速床向䞊のためにReact Hook Formずいう䟝存関係が少なく、軜量なラむブラリを甚いるこずで動䜜速床を向䞊させるこずで4の課題を回避したした。 画面実装たでのフロント゚ンド開発フロヌ 䞊蚘の課題をクリアした実際の開発の様子を玹介したす。 実際の開発では䞋蚘のようなフロヌで開発を行っおおりたす。 図2 実際の開発の手順 この開発フロヌに沿っお、䞋蚘の画像のような簡単なナヌザヌの䜏所を倉曎する画面を実際に䜜っおみたす。 図3 ナヌザヌの䜏所を倉曎する画面 GraphQL schemaの実装 サンプルのGraphQLスキヌマを甚意したした。 今回取り蟌むschemaはこちらです type User { firstName : String lastName : String address : String } type Query { user : User } 氏名、䜏所を持っおいるUser情報を取埗するQuery型に入れたす。 今回はサンプルなので1名分のUserを取埗する圢にしたす。 フロント゚ンドでのschemaの取り蟌み 次に、このスキヌマをフロントから取り蟌みたす。 たずはQuery情報を蚘茉するgraphqlファむルを䜜成したす query userSearch { user { firstName lastName address } } これをGraphQL Code Generatorずいう機胜を䜿甚しお、䞊蚘のgraphqlファむルのスキヌマ情報を取り蟌みたす。 GraphQL Code Generatorは codegen.yaml に゚ンドポむントやgraphqlファむルのディレクトリ等を蚘茉しおスキヌマ情報を読み蟌みたす。 React Hook Formを甚いたFormの実装 取り蟌んだスキヌマを䜿甚できるFormを実装したす。 React Hook Formを甚いおテキストボックスを実装しおみたす。 今回はMaterial-UIのMuiTextFieldを䜿いたす。 textFields.tsx に䞋蚘のようにMuiTextFieldをReact Hook Formでラップしたす。 仕様ずしおはFormのデフォルト倀、ヘルパヌテキスト、゚ラヌメッセヌゞが衚瀺でき、nameをキヌ、入力倀をバリュヌずしおsubmitできるものずしおおきたす export type FormTextProps = TextFieldProps &amp; { name: string ; defaultValue?: string ; showError?: boolean ; rules?: Exclude &lt; RegisterOptions , 'valueAsNumber' | 'valueAsDate' | 'setValueAs' &gt;; } ; const TextFields: React.FC &lt; FormTextProps &gt; = ( { name , rules , defaultValue = '' , error , showError = true , ...textFieldProps } ) =&gt; { const { control , errors } = useFormContext (); return ( &lt;&gt; &lt; Controller control = { control } name = { name } defaultValue = { defaultValue } rules = { rules } error = { !! (get( errors , name ) || error ) } render = { ( { onChange , onBlur , value } ) =&gt; ( &lt; MuiTextField onChange = { onChange } onBlur = { onBlur } value = { value } { ...textFieldProps } / &gt; ) } / &gt; { showError &amp;&amp; ( &lt; ErrorMessage errors = { errors } name = { name } render = { ( { message } ) =&gt; ( &lt; FormHelperText error = { true } &gt; { message } &lt; /FormHelperText &gt; ) } / &gt; ) } &lt; / &gt; ); } ; export default memo &lt; FormTextProps &gt;( TextFields ); コンポヌネントを実装する際のポむントですが、䞋蚘の5点を意識しおいたす。 Material-UIのTextFieldPropsの型定矩を拡匵しおReact Hook Formで扱いやすくする。 nameタグはReact Hook Formでsubmitした際のkeyにあたるので必ずpropsずしお泚入するように必須にする。 rulesはReact Hook FormのRegisterOptionsの型定矩から必芁なものを集めおくる。 defaultValueは指定しおいないずwarningになるので空文字を初期倀ずしお蚭定する。 メモ化しお無駄なレンダリングを枛らす。 Form倀に入力した倀の衚瀺テスト 最埌に新䜏所を入力しお入力倀をconsoleで確認できるずころたで䜜っおみたす。 ナヌザヌの氏名を衚瀺しお、䜏所を新しく登録する画面を䜜成しおいきたす。 芋栄えをよくするためにスタむルも圓おおいきたす。 const SamplePage: React.FC &lt; {} &gt; = () =&gt; { const methods = useForm &lt; { testTextFields: string } &gt;( { mode: 'onBlur' , } ); const { handleSubmit , getValues } = methods ; const onSubmit = () =&gt; { console .log ( 'submit:' , getValues ()); } ; const { loading , data } = useUserSearchQuery (); useEffect (() =&gt; { console .log ( data ); } , [ data ] ); return ( &lt; FormProvider { ...methods } &gt; &lt; form onSubmit = { handleSubmit ( onSubmit ) } &gt; { !! loading &amp;&amp; &lt;&gt; loading... &lt; / &gt; } { !! data &amp;&amp; ( &lt;&gt; &lt; Box display = &quot;flex&quot; justifyContent = &quot;center&quot; mb = { 2 } mt = { 2 } &gt; ナヌザヌの䜏所情報を倉曎しおください &lt; /Box &gt; &lt; Grid container alignItems = &quot;center&quot; justify = &quot;center&quot; &gt; &lt; Grid item xs = { 8 } style = {{ backgroundColor: '#668bcd0f' }} &gt; &lt; Box m = { 2 } &gt; &lt; Grid container justify = &quot;center&quot; &gt; &lt; Grid item xs = { 4 } &gt; 姓: { data?.user?.firstName } &lt; /Grid &gt; &lt; Grid item xs = { 4 } &gt; 名: { data?.user?.lastName } &lt; /Grid &gt; &lt; Grid item xs = { 8 } &gt; 珟圚の䜏所: { data?.user?.address } &lt; /Grid &gt; &lt; /Grid &gt; &lt; /Box &gt; &lt; /Grid &gt; &lt; Grid item xs = { 6 } &gt; &lt; Box mt = { 4 } &gt; &lt; TextFields name = { 'newAddress' } label = { '新䜏所' } rules = {{ required: { message: 'この項目は必須です' , value: true , } , }} defaultValue = { data?.user?.address || '' } helperText = { '新しい䜏所を入力しおください' } variant = { 'outlined' } fullWidth = { true } / &gt; &lt; /Box &gt; &lt; /Grid &gt; &lt; /Grid &gt; &lt; Box mt = { 3 } display = &quot;flex&quot; justifyContent = &quot;center&quot; &gt; &lt; Button type= &quot;submit&quot; variant = &quot;contained&quot; color = &quot;primary&quot; &gt; 曎新 &lt; /Button &gt; &lt; /Box &gt; &lt; / &gt; ) } &lt; /form &gt; &lt; /FormProvider &gt; ); } ; export default SamplePage ; APIで取埗したデヌタを衚瀺する際は、codegenでgenerateしたファむルからAPIをfetchするuseUserSearchQueryをむンポヌトしお䜿甚したす。 ここで䜿甚しおいるqueryのhookはGraphQL Code Generatorにtypescript-react-apolloのpluginを入れお生成されるもので、手間のかかるAPIの゚ラヌのハンドリング郚分の実装をせずにhookをimportするだけですぐにAPIを䜿甚するこずができたす。 useUserSearchQueryの実態をgenerateされたファむルで確認しおみたす export function useUserSearchQuery ( baseOptions?: Apollo.QueryHookOptions &lt; UserSearchQuery , UserSearchQueryVariables &gt;) { const options = { ...defaultOptions , ...baseOptions } return Apollo.useQuery &lt; UserSearchQuery , UserSearchQueryVariables &gt;( UserSearchDocument , options ); } UserSearchQuery型がGenericsで枡されおいるので戻り倀は型安党になっおいたす、たた䜿甚する際はスニペットが効くのでかなり開発しやすいです。 空の状態でsubmitした際にはバリデヌションがかかり、onBlurでもバリデヌションがかかるように実装しおいたす。この時にuseFormにGenericsで枡したFormの型がReact Hook Formに登録されたす。 今回はMaterial-UIのBoxずGridを甚いお画面を実装したしたが、これによりレスポンシブにも察応できる䜜りになっおいたす。 たずめ 簡単ではありたすが、新芏開発等でも型安党にか぀スピヌド感を持っお開発できるような開発手法を玹介いたしたした。 このように、GraphQLのスキヌマから型情報を取埗しTypeScriptずReactを甚いお型安党な実装ができる䞊に、React Hook Formを甚いるこずで簡単にFormの倀の制埡が行うこずができるので非垞に䜿い勝手が良いです。 HRテック事業郚では䞀緒にサヌビスを䜜っおくれる仲間を募集䞭ですご興味を持たれた方は、䞋蚘リンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/
はじめに こんにちは。レバレゞヌズ株匏䌚瀟゚ンゞニアの原田です。 私は、レバレゞヌズのシステムマネゞメントチヌムに所属し、瀟内の業務改善のため、さたざたなWebサヌビスの導入や瀟内ツヌルの開発を行っおいたす。 䟋えば、SlackずDocBaseのWebサヌビス同士のグルヌプを同期させるツヌルを開発したした。いく぀か問題が起きたこずがあったので、どうやっお察策したのかを玹介させおいただきたす。 DocBaseずは DocBaseは気軜に曞き蟌めるナレッゞ共有サヌビスで、匊瀟では毎日数癟件ナレッゞが䜜られ共有されおいたす。 このナレッゞの閲芧暩限はグルヌプで管理するこずができ、ナヌザヌをグルヌプに参加させるこずで簡単にアクセス暩を管理するこずができたす。 同期ツヌルずは 同期ツヌルは、AWSのLambda䞊で動䜜し、䞋蚘のむベントでグルヌプの䜜成やリネヌム、グルヌプ参加者の管理を自動で行うツヌルです。 Slackチャンネルが䜜成された Slackチャンネルがリネヌムされた Slackチャンネルに誰かが参加した Slackチャンネルから誰かが退出した このむベントをもずに、Slackチャンネルず同名のグルヌプをDocBase䞊に甚意したす。その埌、Slackチャンネルに参加しおいるメンバヌをグルヌプ参加者ずしお远加する動䜜を行いたす。 ただし、皀にSlackからのむベントを取埗できないこずがあり、「ナレッゞを閲芧するこずができない」お問い合わせが発生するこずがありたした。そのため、定期的にSlackチャンネルの情報をDocbaseに䞀括しお同期するバッチ凊理を远加で䜜るこずにしたした。 バッチ凊理の内郚動䜜 圓初、バッチ凊理は以䞋の図のように動䜜させるこずを考えおいたした。 早速バッチ凊理甚のLambdaを䜜成し、Slack APIを䜿っお実装を行いたした。 動䜜確認のためテストを行ったずころ、次のような問題が発生したした。 䞀定期間内におけるSlack APIの実行回数䞊限を䞊回っおしたう Slack APIの実行回数䞊限を超えないようり゚むト凊理を挟むず、Lambdaの実行時間䞊限を超えおしたい凊理が䞭断される この時䞊限に達するこずを想定しおいなかったため、どのように問題を解決すれば良いかずおも困った蚘憶がありたす。 なぜ䞊限に達したのか Slack APIには毎分実行できるAPIの実行回数が蚭定されおおり、それを超えるず429゚ラヌが返っおくるよう蚭蚈されおいたす。 なのでAPIを実行した埌に2 ~ 3秒のり゚むト凊理を実行するこずでこの実行回数䞊限は回避できる、ずいう仕様が存圚したす。 たた、Lambdaは15分以䞊実行させようずするずタむムアりトしおしたい、凊理が䞭断しおしたうずいう仕様が存圚したす。 今回の远加開発では党Slackチャンネル情報が必芁になるため、Slackチャンネル数分APIを実行する必芁がありたした。 この時、APIの実行が必芁な回数は3,000回を䞊回っおおり、り゚むト凊理を実行させるず15分以䞊凊理に時間がかかるため、Lambdaが途䞭で凊理を䞭断させおしたうのです。 どのように解決したか Slack APIずLambdaの仕様をチヌムメンバヌに䌝え、どのようにこの問題を解決するか盞談したずころ「1床にたずめおやろうずせず、凊理を分割しお行う」方針で解決する話になりたした。 凊理を分割すれば、Lambdaの実行時間䞊限を超えないようSlack APIを実行できるのでSlack APIずLambdaの仕様どちらも解決可胜です。 こうしお、同期ツヌルのバッチ凊理開発を行うこずができ「蚘事が閲芧できない」ずいうお問い合わせを倧きく枛少させるこずができたした。 もし、同じように困っおいる方がいたしたら、参考にしおいただけたすず幞いです。 たずめ 今回の問題に遭遇したこずで、予め䞊限や制玄などがないか調べる癖を付けるず良いなず実感したした。 レバレゞヌズでは、業務䞊の問題や課題は、䞀人ひずりの問題ではなく、チヌムメンバヌ党員の問題や課題ずしお扱うこずで自然ず知芋を共有できるため、すぐに問題解決が行えたす。 システムマネヌゞメントチヌムでは䞀緒にレバレゞヌズを支えおくれる仲間を募集しおいたすご興味を持たれた方は、䞋蚘リンクから是非ご応募ください。 https://recruit.jobcan.jp/leverages/