TECH PLAY

株式会社RevComm

株式会社RevComm の技術ブログ

171

はじめに RevComm Advent Calendar 2025 1日目の記事です。 qiita.com 昨今では AI コーディングエージェントが話題です。AI コーディングエージェントを活用することで、フロントエンド開発においても生産性の大きな改善が期待できます。 AI コーディングエージェントの活用を広めていくためには、信頼性の高いテストコードがあるとより積極的かつ安全に活用や導入を進めていきやすいのではないかと考えています。 そこで、この記事ではフロントエンド開発において信頼性の高いテストコードを記述するための方法論などについて説明できればと思います。 テストダブルの使用について 前提: テストダブルとは? 大雑把に要約すると、テストにおいて本物のオブジェクトの代わりとして機能してくれるオブジェクトのことを指します。 テストダブルには、スタブ、モック、スパイ、フェイクなど、様々な種類があります。以下の Wikipedia ページに分類が解説されていますが、これらの定義を参考にテストダブルについて紹介します (これらの種別の用法や意味には正確な定義や標準が存在するわけではなく、チームや文脈などによって意味が異なる場合もあると思うため、各用語の意味については参考程度にとどめていただければと思います) en.wikipedia.org ja.wikipedia.org テストダブルの分類 フェイク まず比較的イメージしやすいと思われるフェイクから紹介します。 https://en.wikipedia.org/wiki/Test_double からフェイクの定義を引用します。 Fake — a relatively full-function implementation that is better suited to testing than the production version; e.g. an in-memory  database  instead of a database  server ) 置き換え対象の本物のオブジェクトの振る舞いを模倣したオブジェクトがフェイクであると考えられそうです。 例えば、ユーザーの永続化に関する責務を定義した UserRepository インターフェースがあったとします。これに対して、 ProductionUserRepository は本番コードでの利用が想定された UserRepository の実装であり、REST API を使用してオブジェクトを永続化します。 class ProductionUserRepository implements UserRepository { constructor ( client ) { this .#client = client; } async get ( id ) { try { const response = await this .#client.getUserById(id); return this .#makeUserFromResponse(response); } catch (error) { if (isNotFoundError(error)) return Promise . reject ( new UserNotFoundError(id)); throw error; } } async add ( user ) { await this .#client.updateUser(user); } #makeUserFromResponse () { // 省略... } } これに対して以下はフェイクの実装例です。データはインメモリで管理され永続化は行われないものの、外から見た際には ProductionUserRepository と概ね同じように動作をします。 class FakeUserRepository implements UserRepository { #userById = new Map (); get ( id ) { const maybeUser = this .#userById. get (id); if (maybeUser == null ) return Promise . reject ( new UserNotFoundError(id)); return Promise . resolve (maybeUser); } add ( user ) { this .#userById. set (user. id , user); return Promise . resolve (); } } 依存性の注入 (DI) と併用することにより、実際にコードを動作させる際は ProductionUserRepository を利用し、テストコードの実行時はフェイク実装である FakeUserRepository の方を利用するなど、柔軟に実装を切り替えることができます。 { // 本番コード const service = new UserService ( new ProductionUserRepository ( client )) ; // ... } { // テストコード const service = new UserService ( new FakeUserRepository ()) ; // ... } フェイクを実装する際は、本番向けの実装とフェイク実装とで共通のテストコードを実行しておくと、それぞれの振る舞いを維持することができ、より高い信頼性が期待できます。後述する Google のソフトウェアエンジニアリング においてもこの手法は推奨されており、 https://en.wikipedia.org/wiki/Test_double においては Verified fake と呼ばれています。 フェイクには他のテストダブルと比較して信頼性が高いというメリットがあります。しかし、デメリットとして、後述するスタブやモックなどと比較すると実装コストが高くなりがちであり、またメンテナンスも必要です。このフェイクの実装やメンテナンス作業というのはまさに AI コーディングエージェントが得意としている分野であると考えられるため、もしフェイクの利用を進める場合はぜひ活用を検討してみると良さそうです。 スタブ https://en.wikipedia.org/wiki/Test_double から定義を引用します。 Stub — provides static input 定義に基づいて考えると、スタブは以下のように実装することができます。ライブラリーなどを使用せずとも比較的容易に実装が可能で、フェイクと比較すると、実装がかなり簡単です。 class StubUserRepository implements UserRepository { get ( id ) { return Promise . resolve ( new User(id, "foobar" )); } add ( _user ) { return Promise . resolve (); // NOOP } } JavaScript は動的言語であり、例えば Jest の jest.spyOn() を使うと特定のメソッドのみをスタブに置き換えることも容易に行えます。 jest . spyOn ( localStorage , 'getItem' ) . mockImplementation (() => 'foo' ) ; スタブは実装がとても容易であるというメリットがありますが、先に紹介したフェイクと比較すると信頼性においては大きく劣るというデメリットがあります。乱用はし過ぎずに適度な利用がおすすめであると考えます。 モック https://en.wikipedia.org/wiki/Test_double から定義を引用します。 Mock — verifies output via expectations defined before the test runs 上記のモックの定義に従うと、 Sinon.JS における mock() が定義に近いと思います。 // ... const mock = sinon . mock ( userRepository ) ; // Expectations mock . expects ( 'add' ) . once () . withArgs ( user ) ; const userService = new UserService ( userRepository ) ; await userService . add ({ id : '1' , name : 'foobar' }) ; mock . verify () ; このように JavaScript においては、Sinon.JS や testdouble.js などのライブラリーを使うと比較的簡単にモックを実装できます。依存しているオブジェクト間でのコミュニケーションを検証することで、意図した副作用が発生していることをテストすることができます。例えば、特定のメソッドがある順番に従って呼び出されていることを検証したいケースにおいて利用ができます。ただし、テスト対象がどの順番で一連のメソッドを呼び出すかは実装の詳細であり、大抵の場合、過度にモックを乱用したり依存することは望ましくないと考えられます。 モックは便利な仕組みではあると思いますが、契約による設計が言語レベルでサポートされているような稀なケースを除いて、モックに過度に依存しすぎるのは避けた方が良いと筆者は考えています。特定のメソッドが呼ばれたかどうかを検証するのではなく、それによって引き起こされる状態遷移などを検証した方がテストの信頼性が高まると思います。 スパイ https://en.wikipedia.org/wiki/Test_double から定義を引用します。 Spy — supports setting the output of a call before a test runs and verifying input parameters after the test runs スタブやモックなどとの違いが少しややこしいですが、テストの実行後に入力パラメーターの検証が行えるという点が大きな違いであると思います。 スパイについても JavaScript では jest.fn() や vi.fn() , jest.spyOn() などを利用すると容易に実装できます。 const stubUserRepository: UserRepository = { get : jest.fn(( id ) => new User(id, "foobar" )), // スタブ add : jest.fn(), // スパイ } ; doSomethingWithUserRepository(stubUserRepository); expect (stubUserRepository. add ).toHaveBeenCalledWith( new User(id, "foobar" )); // 入力値の検証 どれを使えばいいの? テストダブルの使い分けについては基準が難しいところではありますが、まず前提として、テストダブルを使わなくともテストを書くことが可能なのであれば、それがコードが意図した通りに動作していることを保証するための最も信頼性の高い方法であると思います。ただし、現実には外部の REST API や サービスなどに対してテストから直接接続する場合、意図せぬ副作用が発生してしまったり、テストの実行時間が大きく増加してしまうことなども考えられます。そのような場合はテストダブルの使用を検討すると良いでしょう。 参考までに、 Google のソフトウェアエンジニアリング という本の13章においてテストダブルの使用に関して非常に詳しく解説されています。 この本ではまず「忠実性」の考えについて紹介されています。「忠実性」とはテストダブルが置き換え対象の本物のオブジェクトの挙動にどれくらい近いかを表す指標であると説明されています。 前提としてテストダブルを使用せずとも十分にテストが可能である際は、テストダブルを使用せずに本物のオブジェクトを使用することがこの本でも推奨されています。それがコード上に存在するバグを正しく検出してくれる可能性が高いケースが多いと考えられるためです。 もしテストダブルの使用が必要である際はフェイクの使用が推奨されています。フェイクは本物のオブジェクトの挙動を模倣したものであり、スタブやモックなどと比較して忠実性が高いためです。逆にスタブやモックを過度に用いることは、フェイクを使用した場合と比較してテスト対象の実装の詳細への依存度が高くなってしまいがちであり、脆いテストコードができてしまうリスクがあると説明されています。 jest.mock() / vi.mock() の使用はできるだけ避ける これらの前提に基づいて、まずは Jest における jest.mock() や Vitest における vi.mock() について考えてみます。これらの API は先ほどのテストダブルの定義に照らし合わせた場合、スタブに該当するものであると考えられます。そのため、フェイクと比較すると忠実度が低く、乱用しすぎると信頼性が低いテストコードができてしまう原因になってしまう可能性が考えられます。 jest.fn() や 後述する jest.spyOn() などを使用してスタブを実装する場合においても信頼性の低いテストコードができてしまうケースは考えられますが、 jest.mock() や vi.mock() に関しては実装の詳細へ強く依存したテストコードがより一層容易に記述できてしまうため、特に注意が必要であると考えます。 例えば、 apis/getUser モジュールを介して REST API を実行し、ユーザー情報を取得するフックがあったとします。 // src/hooks/useUser.ts import { getUser } from 'apis/getUser' ; export function useUser ( id ) { const [ user , setUser ] = useState( null ); const [ isLoading , setIsLoading ] = useState( false ); const [ error , setError ] = useState( null ); useEffect(() => { if (isLoading) return ; setIsLoading( true ); getUser(id) . then (( user ) => setUser(user)) . catch (( error ) => setError(error)) . finally (() => setIsLoading( false )); } , [ id ] ); return { error , user , isLoading } ; } jest.mock() を使うことにより、非常に簡単にスタブをセットアップすることができます。 // src/hooks/__tests__/useUser.spec.tsx import { renderHook, waitFor } from '@testing-library/react' ; import { useUser } from '@/hooks/useUser' ; jest.mock( 'apis/getUser' , () => { return { getUser : ( _id ) => Promise . resolve (dummyUser), } ; } ); test ( 'useUser()' , async () => { const { result } = renderHook(() => useUser(dummyUser. id )); expect (result. current .isLoading).toBe( true ); await waitFor(() => expect (result. current .user).toEqual(dummyUser)); expect (result. current .isLoading).toBe( false ); } ); jest.mock() を使うことにより、見かけ上はシンプルにテストを記述することができました。ではこれの何が問題なのでしょうか? 例として、ここで apis/getUser モジュールを apis/users/get にリネームしたとします。それに伴い、 src/hooks/useUser.ts の import も修正する必要があります。 // src/hooks/useUser.ts - import { getUser } from 'apis/getUser'; + import { getUser } from 'apis/users/get'; export function useUser(id) { const [user, setUser] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { if (isLoading) return; setIsLoading(true); getUser(id) .then((user) => setUser(user)) .catch((error) => setError(error)) .finally(() => setIsLoading(false)); }, [id]); return { error, user, isLoading }; } ソースコードは適切に修正されており、プロダクションコードは意図通りに機能し続けます。 しかし、この状態で src/hooks/__tests__/useUser.spec.tsx を実行すると、テストは失敗してしまいます。 apis/getUser.ts から apis/users/get.ts へのリネームは適切に行われており、 getUser() の振る舞いも変わってはいないので、本来であればこの状況でテストが失敗してしまうことは望ましくありません。これはテストコードが脆い状態に陥ってしまっており、信頼性が低下してしまっていることを示唆しています。 この問題が発生してしまうのは、 src/hooks/__tests__/useUser.spec.tsx が テスト対象である src/hooks/useUser.ts モジュールの実装の詳細である「モジュール間の依存関係」に強く依存してしまっていることが原因です。 jest . mock ( 'apis/getUser' , () => { return { getUser : ( _id ) => Promise . resolve ( dummyUser ) , } ; }) ; モジュール間の依存関係というのは、実装の詳細の中でもかなり詳細度の高いものであると考えられるため、これにテストコードが依存してしまうことは望ましくないと考えます。 改善案 Testing Library (詳細は後述します) がコンポーネントの実装の詳細への強い依存を避けることでテストコードの信頼性を高めることを重視していることと同様に、この問題においてもテストコードがテスト対象の実装の詳細へ強く依存しすぎてしまうことを避けることで改善することができます。具体的に2つの解決策について紹介します。 1. ユーザーの取得に関する責務を抽象化する (フェイクを使用した改善例) useUser() において重要なのは、何かしらの手段によってユーザー情報を取得し、それに関する状態管理を行うことです。どのようにしてユーザーを取得するかについては実装の詳細にあたり、重要ではないと考えられます。 そこで、このユーザー情報の永続化に関する責務を表現する interface を用意します。 export interface UserRepository { get ( id : UserID ): Promise < User > ; add ( user : User ): Promise < void > ; } UserRepository の実装は Context を介して注入するように変更します。 // src/hooks/useUser.ts import { useContext } from 'react' ; export function useUser ( id ) { const [ user , setUser ] = useState( null ); const [ isLoading , setIsLoading ] = useState( false ); const [ error , setError ] = useState( null ); const userRepository = useContext(UserRepositoryContext); useEffect(() => { if (isLoading) return ; setIsLoading( true ); userRepository. get (id) . then (( user ) => setUser(user)) . catch (( error ) => setError(error)) . finally (() => setIsLoading( false )); } , [ id ] ); return { error , user , isLoading } ; } こうすることで、テスト時はフェイク実装によって代用することが可能です // src/hooks/__tests__/useUser.spec.tsx import { renderHook, waitFor } from '@testing-library/react' ; import { useUser } from '@/hooks/useUser' ; import { FakeUserRepository } from '@/repositories/user.fake.ts' ; test ( 'useUser()' , async () => { const userRepository = new FakeUserRepository(); await userRepository. add (dummyUser); const { result } = renderHook(() => useUser(dummyUser. id ), { wrapper : ( { children } ) => ( < UserRepositoryContext.Provider value = {userRepository} > { children } </ UserRepositoryContext.Provider > ), } ); expect (result. current .isLoading).toBe( true ); await waitFor(() => expect (result. current .user).toEqual(dummyUser)); expect (result. current .isLoading).toBe( false ); } ); このように interface によって責務を抽象化し、依存注入によって依存関係を取り扱うパターンはフレームワークとして Angular などを採用されている場合は比較的一般的なパターンではないかと思われます。しかし、そうではない場合は ここまでやらなくても十分なケースも多いと思われるため、最終的にはプロジェクトの規模やチームの方針などに応じて決めると良いと思います。 ここではもう一つの方法として msw を使った方法についても紹介します。 2. mswを使う msw とは HTTP に関するスタブライブラリーです。 github.com 元の例における src/hooks/__tests__/useUser.spec.tsx は src/hooks/useUser.ts が apis/users/get.ts に依存しているということを前提に記述されていました。モジュール間の依存関係というのは、実装の詳細の中でもかなり詳細度が高いものであると考えられ、テストコードがその詳細に依存することによって信頼性が低下してしまっています。msw を使うことによって、テストコードが「このコードは apis/users/get.ts モジュールに依存し、それを使って HTTP リクエストを送信している」という強い前提への依存から「このコードは何らかの方法で HTTP リクエストを送信している」という前提への依存へ緩めることができます。 // src/hooks/__tests__/useUser.spec.tsx import { renderHook, waitFor } from '@testing-library/react' ; import { useUser } from '@/hooks/useUser' ; import { http, HttpResponse } from 'msw' ; import { setupServer } from 'msw/node' ; const server = setupServer( http. get ( '/api/users/:id' , () => HttpResponse.json(dummyUser)), ); beforeAll (() => server.listen( { onUnhandledRequest : 'error' } )); afterEach (() => { server.resetHandlers(); } ); afterAll (() => server. close ()); test ( 'useUser()' , async () => { const { result } = renderHook(() => useUser(dummyUser. id )); expect (result. current .isLoading).toBe( true ); await waitFor(() => expect (result. current .user).toEqual(dummyUser)); expect (result. current .isLoading).toBe( false ); } ); jest.mock() を使用した例と比較して記述量は増えるものの、大きなメリットとして、 useUser() の実装を TanStack Query などを使用して書き換えたとしても、そのままテストが動作してくれます (ただし、 renderHook を呼ぶ際の Provider の指定は必要です) msw を使用する際は意図せぬ API リクエストの送信を検知できるよう、 onUnhandledRequest に error の設定をオススメします。 beforeAll (() => server . listen ({ onUnhandledRequest : 'error' })) ; msw を使うことにより、 jest.mock() を使用した場合と比較して、テストコードがテスト対象コードの実装の詳細へ強く依存しすぎてしまう状況を緩和することができました。 ただし、この msw を使用した例においてもテストコードは依然として「テスト対象が何らかの方法で HTTP リクエストを送信する」という実装の詳細へ依存した状態です。1つ目のフェイクを使用した改善例と比較すると実装の詳細への依存度は高い状態であると考えられます。しかし、あまりにも抽象化することを意識しすぎてしまうと、今度は過度な抽象化を招いてしまうリスクも考えられます。フロントエンド開発においては、大抵の場合はこの msw を使用してスタブをセットアップする解決策で十分なケースが多いのではないかと考えています。 また、msw を利用する場合も、可能であれば本物の API の振る舞いに近づけるとより信頼性が高まることが期待されます。必要に応じて検討すると良いでしょう。 const users = [] ; const server = setupServer( http. get ( '/api/users/:id' , ( { params } ) => { const user = users. find (( x ) => x. id .equals(params. id )); if (user == null ) return new HttpResponse( null , { status : 404 } ); else return HttpResponse.json(serializeUser(user)); } ), http.post( '/api/users' , async ( { request } ) => { const { name } = await request.json(); const id = makeUserID(); users. push ( new User(id, name )); return HttpResponse.json( { id , name } ); } ), ); jest.mock() / vi.mock() の使用が適したケースについて 筆者としては jest.mock() の使用は極力避けた方が良いとは考えていますが、まだテストコードが導入されておらず、これから導入していきたいというようなケースにおいては、どうしても jest.mock() を使わないとなかなかテストを追加することが難しいというような場合もあると思います。そういったケースにおいては jest.mock() は非常に便利な機能であると思うため、用途や場面を限定して使用すると良いと思っています。 jest.spyOn() / vi.spyOn() の使用について 先ほどの定義に基づいて考えると、 jest.spyOn() や vi.spyOn() はスタブやスパイなどのセットアップに利用できる機能です。つまり、テストにおいて本物のオブジェクトを使用する場合やフェイクを使用する場合と比較して、忠実性は低下してしまいます。 例として localStorage に依存した useSidebar() フックをテストするケースについて考えてみます。 function useSidebar () { const [ isCollapsed , _setIsCollapsed ] = useState(() => JSON . parse (localStorage. getItem ( 'isSidebarCollapsed' ) ?? 'false' )); const setIsCollapsed = useCallback(( isCollapsed ) => { localStorage. setItem ( 'isSidebarCollapsed' , JSON . stringify (isCollapsed)); _setIsCollapsed(isCollapsed); } , [] ); return { isCollapsed , setIsCollapsed } ; } これをテストする場合、例えば jest.spyOn() を使用して localStorage をスタブする方法が考えられます。 test ( 'useSidebar' , async () => { jest.spyOn(localStorage, 'getItem' ).mockImplementation(() => 'true' ); const setItem = jest.spyOn(localStorage, 'setItem' ); const { result } = renderHook(() => useSidebar()); expect (result. current .isCollapsed).toBe( true ); expect (setItem).not.toHaveBeenCalled(); act(() => result. current .setIsCollapsed( false )); expect (result. current .isCollapsed).toBe( false ); expect (setItem).toHaveBeenCalledTimes( 1 ); } ); 上記の例では jest.spyOn() を使用して localStorage をスタブしていますが、 jsdom には localStorage のフェイク実装がすでに含まれており、こちらに依存した方がより信頼性が高まるでしょう。特に localStorage は Web 標準の API であり、その振る舞いや API に破壊的変更が生じる可能性は比較的低いと考えられます。また、 localStorage はネットワークアクセスが発生するわけでもなく、高速に動作することが期待されます。そのため、わざわざフェイク実装やスタブを用意せずとも比較的信頼性の高いテストが書けそうです。 afterEach (() => localStorage. clear ()); test ( 'useSidebar' , async () => { localStorage. setItem ( 'isSidebarCollapsed' , 'true' ); const { result } = renderHook(() => useSidebar()); expect (result. current .isCollapsed).toBe( true ); act(() => result. current .setIsCollapsed( false )); expect (result. current .isCollapsed).toBe( false ); expect (localStorage. getItem ( 'isSidebarCollapsed' )).toBe( 'false' ); } ); このようにスタブを使用せずに、実際のオブジェクトとのやりとりも含めたインテグレーションテストを記述することで、より信頼性の高いテストが書けるケースもあります。 Testing Library を使ってコンポーネントのインテグレーションテストを記述する 次はコンポーネントに対するテストの観点から考えてみます。 Enzyme / Vue Test Utils について コンポーネントのテストという観点では、React においては Enzyme 、Vue.js においては Vue Test Utils のような高機能なテスト用パッケージがあります。 これらのパッケージはレンダリングされたコンポーネントの状態を直接問い合わせたり、CSS セレクターによるレンダリング結果の柔軟な問い合わせなどをサポートしてくれる便利なライブラリーです。 これらのライブラリーを使用して信頼性の高いテストコードを書くことも可能ではあると思います。しかし、そのためには注意深くテストの記述やレビューなどを行う必要があります。 React Testing Library や Vue Testing Library などのいわゆる Testing Library では最初から信頼性を念頭に置いて設計されており、これらのライブラリーを利用することでより信頼性の高いテストコードを記述しやすいです。 なぜ Testing Library を使うのか? 公式の Guiding Principles で解説されていますが、Testing Library の考えとして、テスト対象のコンポーネントに対して「ユーザーは実際にブラウザー上でどのようにして対話するか?」という観点からテストを記述できるようにすることで、テストコードが対象コンポーネントの実装の詳細に依存することを回避し、より信頼性の高いテストコードを記述できるようにしてくれます。 github.com Testing Library の説明は Web 上にすでにたくさん存在しているため、簡潔に特徴を紹介します。 例えば、Enzyme においてはコンポーネントのレンダリング結果に対して CSS セレクターを使用して柔軟に問い合わせを行うことが可能です。 const wrapper = shallow(< MyForm />); wrapper. find ( '.some-button' ).simulate( 'click' ); それに対して Testing Library では CSS セレクターによるレンダリング結果の問い合わせ機能が意図的に提供されていません。その代わりにアクセシビリティーの観点から問い合わせることが推奨されています。以下は React Testing Library を使用した例です。 import { render } from '@testing-library/react' ; import { userEvent } from '@testing-library/user-event' ; test ( 'MyForm' , () => { const user = userEvent.setup(); const screen = render(< MyForm />); // button ロールを持ち Save というアクセシブル名をもつ要素を問い合わせ、それをクリックします await user.click( screen .getByRole( 'button' , { name : 'Save' } )); } ); ユーザーが実際に UI を操作する際は CSS セレクターという実装の詳細に基づいて要素を認識しているわけではなく、個々の要素の役割やラベルなどに基づいて操作します。Testing Library ではこの考えに基づき、意図的に CSS セレクターによる問い合わせを禁止し、代わりにアクセシビリティーに基づいた問い合わせを推奨しています。 また、CSS セレクターによる問い合わせを避けることで、例えば、クラス名がリネームされた際に意図せずテストが失敗してしまうことを防止できます。 また、Enzyme においてはコンポーネントの現在の状態を直接問い合わせたり、またはコンポーネントで定義されたメソッドを直接呼ぶことも可能でした。しかし、コンポーネントの状態や定義されたメソッドというのは実装の詳細です。例えば、コンポーネントの状態がリネームされた場合、それにテストコードが依存していた場合、テストは容易に壊れてしまいます。 const wrapper = shallow(< MyForm />); wrapper. find ( '.some-input' ).simulate( 'click' ); expect (wrapper. state ( 'isSaving' )).toBe( true ); // コンポーネントの状態を直接問い合わせる (もし state がリネームされると、このテストは失敗します) Testing Library ではこのようなコンポーネントの状態の問い合わせやメソッドの直接的な呼び出しも廃止されており、先ほどの CSS セレクターの例と同様にアクセシビリティーの観点からレンダリング結果を問い合わせたり、要素を操作することによって対応することが想定されています。 const user = userEvent.setup(); const screen = render(< MyForm />); await user.click( screen .getByRole( 'button' , { name : 'Save' } )); expect ( await screen .findByRole( 'img' , { name : 'Saving' } )).toBeVisible(); // アクセシビリティーに基づいてレンダリング結果を問い合わせる このように Testing Library では意図的に実装の詳細への依存を回避することで、高い信頼性を提供してくれます。 Testing Library を利用する際の Tips どのクエリメソッドを使うべきか? 詳細については公式ドキュメントに記載されていますが、基本的には ByRole クエリーを使用して問い合わせをすることが推奨されています。 github.com 具体的には、以下の場合、 Save というアクセスシブル名を持つ button ロール を問い合わせます (大抵の場合、 Save というラベルが設定された button が見つかることでしょう) screen .getByRole( 'button' , { name : 'Save' } ); ByRole クエリーを使用することで、特定の要素をユーザーが特定・操作する際の意図に基づいて問い合わせることができます。極端な例ではありますが、ある UI コンポーネントフレームワークから別の UI コンポーネントフレームワークへ移行したとしても、 ByRole クエリーに基づいて要素を問い合わせていれば、ある程度はテストコードを変更せずにそのまま動作し続けてくれることが期待できます。 もし ByRole クエリーを利用することが難しい場合は、 ByLabelText または ByPlaceholderText などの代替手段を利用すると良いと思います。 ByTestId はどうしても他の手段では要素を問い合わせることが困難な場合に限定して使用すると良いです。 イベントを発火させる際は @testing-library/user-event を使う 例えば、React Testing Library には fireEvent() というAPIがあり、要素に対して任意のイベントを発火させることが可能です。例えば以下のように記述することで、特定要素に対して click イベントを発火させることができます。 fireEvent.click(someButton); しかし、実際にユーザーがブラウザーをマウスで操作してクリックする際は、まずマウスによってカーソルをボタンの上部まで移動させ 、その後、マウスの左キーをクリックするといったように、実際にはその背後では様々なイベントが発火されています。 @testing-library/user-event パッケージは、このようなユーザーが実際にブラウザー上で特定の要素を操作する際の一連の振る舞いを可能な限り再現してくれるパッケージです。 import { userEvent } from '@testing-library/user-event' ; // ... const user = userEvent.setup(); const someButton = screen .getByRole( 'button' , { name : 'Foo' } ); await user.click(someButton); 要素を操作する際は fireEvent() ではなく @testing-library/user-event を使用することで、より信頼性が改善されることが期待できます。 バグの修正時にはリグレッションテストを記述する 長年、プロダクトの開発や運用を続けていると、どうしてもバグ修正などが積み重なった結果、意図の不明瞭なコードなどができてしまいがちです。 しかし、AI コーディングエージェントはそれらの背景の情報を持っておらず、意図せずそういった不明瞭なコードを書き換えてしまう可能性が考えられます。 そういったケースへの対策や、バグの再発防止などのために、リグレッションテストを記述するとより安心して運用が行いやすくなると思います。 具体的には、あるバグが発見された際には、まずそのバグを再現するためのテストコード (リグレッションテスト) を記述すると理想的です。 例えば、与えられた数値の合計値を求める sum() を例に考えてみます。 const sum = (... numbers ) => numbers. reduce (( x , y ) => x + y); この sum() はある特定の状況下で例外が発生してしまうことが発覚しました。具体的には引数が一つも与えられていない場合に例外が起きてしまいます。この場合、 0 が返却されると望ましそうです。 sum() を修正する前にまずはバグを再現するテストコードを記述します。 test ( 'Regression test for issue #1234' , () => { expect (sum()).toBe( 0 ); } ); この状況でこのテストコードを実行してみます。失敗した場合、意図通りにテストコードによってバグを再現できています。 それでは実際にこのバグを修正してみます。 const sum = (... numbers ) => numbers. reduce (( x , y ) => x + y, 0 ); 修正後、再度テストコードを実行し、今度はテストが成功することを確認します。 こうすることにより、追加したリグレッションテストコードがバグの再発防止のための仕組みとして機能してくれます。もし AI コーディングエージェントによって本来の意図を損なう形でコード変更が行われてしまった際も、CI でテストコードを自動実行しておけば、事前に気づくことができます。 今回は解説のために単純な関数を使用した例で紹介しましたが、実際には React Testing Library などを活用することで、UI のバグなどに関するリグレッションテストを記述することも可能です。 まとめ ブラックボックステストを意識する テストダブルや Testing Library などを例に、信頼性の高いテストコードを記述するための方法について紹介しました。本番コードと同様にテストコードにおいても実装の詳細に強く依存してしまうと、テストコードの信頼性が低下してしまうことがあります。できる限りブラックボックステストとして記述することを意識すると良いと思います。 より安定したものに依存する テストコードにおいても本番コードと同様により安定したものに依存することを意識すると、信頼性を高めることが期待できます。 具体的には、サードパーティーライブラリーは「変わりやすいもの」の最たる例ではないかと思います。サードパーティーライブラリーに依存したテストコードを記述する際は、サードパーティーライブラリーが提供する API に対して jest.spyOn() や jest.mock() などを使用してスタブするよりも、以下の方法などを検討すると良いでしょう。 サードパーティーライブラリーが提供する API をスタブせずにインテグレーションテストを記述する サードパーティーライブラリーによって達成したい目的に基づいて interface を定義し、テスト対象コードをサードパーティーライブラリーではなくその interface に依存させる (これによってフェイクオブジェクトを注入したり、サードパーティーライブラリーの API に破壊的変更が加わった際のテストコードへの影響を避けることが期待できます) 終わりに 本記事で紹介した内容が少しでも役に立てば幸いです。この記事で度々紹介した Googleのソフトウェアエンジニアリング はオススメなので、今回紹介したような内容などに興味があればぜひ参照ください! また、明日も Advent Calendar の記事を公開予定のため、もしご興味があればぜひご覧ください! 参考文献・出典 書籍 『Googleのソフトウェアエンジニアリング ―持続可能なプログラミングを支える技術、文化、プロセス』 Wikipedia "Test double"
アバター
こんにちは、ホセです! 日本語と英語話せるエンジニアです。PyCon JPには初めて参加しました! 本件はPyCon JP って何か、今年の気になったトーク、開発スプリントのテーマと個人感想を書かせていただきます! 今年のテーマ PyConは、Pythonに関する世界最大級のカンファレンスであり、日本では2011年1月末には、品川シーサイドで日本初のPyConである「PyCon mini JP」が開催されて以来、これまで14年間にわたり東京で開催されてきました。 今年は「多様性」をテーマに、初めて広島で行われました! 2025.pycon.jp 日程: 2025年9月26日(金)〜27日(土) 会場: 広島国際会議場 (広島県広島市中区中島町1−5) 主催: 一般社団法人 PyCon JP Association トーク [招待講演】PEP 750 の共同著者 青野高大 氏による Python3.14の新機能の紹介 github.com さすがの招待講演。新たな T-strings の開発者の一人から説明を聞くのが貴重な経験でした。リリースは楽しみですね! Financial analysis in Python 2025.pycon.jp Nicholas氏は用語の説明はうまくてわかりやすかったです!ファイナンスが興味ある方におすすめ! Pythonスレッドとは結局何なのか? ~CPython実装から見るNoGIL時代の変化~ 本日の発表資料です。PyConにてThreadについてのテーマで お話させていただきました。 とてもたくさんの方に聞いて頂きまして大変嬉しかったです。講演の後も10人の質問行列ができるなど、発表してよかったと感じました。 https://t.co/50ehUcvNbD #pyconjp_4 #pyconjp2025 — Shugo Manabe: Recustomer CTO (@curekoshimizu) 2025年9月26日 Manabeさんの深い知識に圧倒され続け、CPythonの歴史に関するお話もとても面白い講義でした。 Weaponizing MCP Servers: Production-Ready AI Agent Infrastructure with Python 2025.pycon.jp 本番に動いているMCPってどんな問題が発生するのかとMicrosoft エンジニアの方からの発表は興味深かったです。 Vijayさんもとてもフレンドリー、レクチャの後いろいろ話させてもらいました! 【Day 1 Keynote】Sebastián Ramírez 氏 Behind the scenes of FastAPI and friends for developers and builders 2025.pycon.jp Sebastian氏は前からYouTubeで発表をフォローしていて、実際にお会いできて一番驚いたのは「お気遣い」でした。 今回は日本での初めての発表ということもあり、分かりやすい英語で話してくれたことにオーディエンスは感謝していました。 発表も素晴らしくてメッセージも簡単でも深い:「Solve a problem」。問題があったら、解決を考えて解決してみろと。本当に解決できたら、いいプロダクトになると。エンジニアって結局プログラムを作って誰かに使って欲しいから、開発する前にちゃんとプロブレムを考えないと時間の無駄になります。 その他 Apache Arrowの Contributorからの説明:  https://2025.pycon.jp/en/timetable/talk/Q7YYNZ Pythonでモバイルアプリを作るの? https://2025.pycon.jp/en/timetable/talk/S8DUBR Django Ninja入門! https://2025.pycon.jp/en/timetable/talk/ZNSAA9 帰宅してからすぐに使わせてもらったpytestの10個のこつ  https://2025.pycon.jp/en/timetable/talk/ZFYREY 開発スプリント スプリントの日はチームを組んでコードを書くのがテーマです。今回は Tachibanaさん のチームに参加して Streamlit へのPRを投げました! github.com オフィシャルパーティー 会議間の枠は15分ぐらいしかないのであまり他の参加者と話す時間が少ないため、パーティーの時は最高でした。広島フードも美味しかったし、いろんな方と話して本当に良かった。 スペイン語圏同士でもあって、Sebastian氏ともいろいろ話しました! 感想 「多様性」というテーマは本当にふさわしいと感じました。外国人参加者は50人以上いたそうで、英語のライブ翻訳もきちんと用意されており、スタッフの皆さんも英語で案内していました。私の場合は日本語で話すことが多かったのですが、英語圏の方と話したときに「日本の温かさを感じた」と言ってもらえたのが印象的でした。 そして、来年の会場も広島に!プロポサールを準備して楽しみにしてます! まとめ 今年のRevcomm エンジニア参加は7名、その中で松土さんが登壇して 陶山さん がスタッフでした。 みんなと面白い時間を過ごして良かったです。
アバター
RevCommで主に音声認識・音声感情認識・話者分離の研究開発を担当している石塚です。 本記事では、 日本音響学会2025年秋季研究発表会 で発表した「Room Simulatorを用いたデータ拡張によるNeural Speaker Diarizationモデルの実環境適応」の研究について、解説します。 石塚賢吉(いしづか けんきち) プリンシパルリサーチエンジニア。筑波大学大学院博士後期課程卒業。博士(工学)。日本HP株式会社にて通信事業者向けのシステム開発、株式会社ドワンゴで全文検索システムの開発などに従事。2019年12月、株式会社RevComm入社。音声認識、音声感情認識、全文検索システムの研究開発を行なっている。 → 過去記事一覧 背景:「いつ、誰が話しているか」の推定は難しい AI技術の進歩は目覚ましく、その恩恵はビジネスシーンにも広がっています。特に、会議の音声を自動でテキスト化するアプリケーションは、議事録作成の効率化や情報共有の促進に大きく貢献しており、導入する企業が増加傾向にあります。これらのアプリケーションの中核を担う技術の一つが、Speaker Diarization (SD) です。これは、音声データの中から「いつ、誰が話しているか」を推定する技術であり、会議の参加者を識別し、発言内容を整理するために不可欠です。 しかし、現実は理想通りとは限りません。会議室の環境は千差万別であり、アプリケーションの利用環境によっては、音声品質が大きく劣化する可能性があります。例えば、話者とマイクの距離が遠い場合、音声が小さくなり、周囲の雑音に埋もれてしまうことがあります。また、室内の残響が大きいと、音声の輪郭がぼやけて聞き取りにくくなります。これらの要因が複合的に作用することで、SDの精度が低下し、結果として、テキスト化された議事録の信頼性が損なわれてしまうという課題がありました。 課題:SDモデルの学習には「大量の教師データ」が必要…でも、作成コストが高い SDモデルを特定の環境、例えば「カフェでの会話をスマートフォンで録音する」のような環境でうまく機能させるためには、その環境で録音された大量の音声データでSDモデルをファインチューンすることが有効です。 しかし、この学習データを作成するには、録音された音声を聞きながら「この区間はAさん、次の区間はBさん…」と、手でラベル付け(アノテーション)することとなり、膨大な時間とコストがかかるという問題がありました 。 解決策:「Room Simulator」でリアルな学習データを人工的に作り出す そこで注目したのが、 「Room Simulator」 という技術です。これは、部屋の広さや反響、マイクと話者の位置関係などをコンピュータ上で再現し、まるでその環境で録音したかのような音声をシミュレートできる技術です。 RevCommは、話者ごとに録音されたビデオ会議の音声データを大量に保有しています。本研究では、話者ごとに録音されたビデオ会議の音声データを元として、 PyRoomAcoustics という、 鏡像法に基づくRoom Simulator で対象の環境を想定した音声の学習データを大量に生成することを考えます。この手法の利点は以下の通りです。 大量の自然な会話データが使える : 合成音声などでなく、実際のビデオ会議の発話録音なので、会話の内容が自然です ラベル付けが不要 : 最初から話者ごとに音声が分かれているため、面倒な手作業のアノテーションが必要ありません 低コストで大量生産 : コンピュータ上でシミュレーションするため、大量の学習データを生成できます 評価実験: データセット構築 提案手法の有効性を確かめるため、まず対面会議をスマートフォンで録音する環境を想定した3種類のデータセットを構築しました。 1. Computer Simulation Dataset (CSD) - 人工的な学習データ MiiTelで行われたビデオ会議(2〜7名)の音声をもとに、会議室とカフェで行われる対面会議をスマートフォンで録音する環境をRoom Simulatorでシミュレートしながら生成した音声のデータセットです。 シミュレートした環境 : - 会議室 : 8畳の部屋 (4m×5m×2.5m) を想定し、反響や音の減衰を再現しました。話者の音源は会議の参加人数に応じて図1の丸の記号に付与された番号の順番で配置します。 仮想会議室の音源とマイクの配置の例 カフェ : 広い空間 (20m×20m×4m) を想定し、Musanデータセットの人混みの環境音や音楽をミックスして、より雑音の多い環境を再現しました。音源とマイクの位置関係は会議室と同じです。 このデータセットの長所と短所: 長所: 高速(AWS EC2 r5.2xlargeインスタンスで音声時間の0.0045倍の処理時間)かつ大量にデータを生成可能です 短所: 現実世界の複雑な音響特性を完全には反映できません 2. Human Annotation Dataset (HAD) - 人間がアノテーションした評価データ 対面会議(30件, 24h)をスマートフォンで録音した音声について、人間が手作業で「いつ、誰が話しているか」をアノテーションして構築した評価用データセットです。これは、最も現実に近い評価用データです。 このデータセットの長所と短所: 長所 : 現実の音響特性を最も忠実に反映できます 短所 : 作成に 音声の長さの約2倍から8倍 もの時間がかかり、コストが非常に大きいです 3. Loudspeaker Simulation Dataset (LSD) - 物理的に再現した評価データ 話者ごとに録音されたビデオ会議(125件, 112h)の音声を、図1の構成で複数のスピーカー装置で再生し、スマートフォンで録音することで、会議室での対面会議をスマートフォンで録音する環境を物理的にシミュレーションしながら構築したデータセットです。この方法は、 LibriCSS データセットの作り方を参考にしています。 このデータセットの長所と短所: 長所 : 低コストでCSDより現実的な録音環境を反映できます 短所 : スピーカーから再生される音声と人間の声道から発せられる音声との違いは反映されず、また録音に実時間分の時間がかかります 評価実験: SDモデルのファインチューニング 次に、PyAnnote Audio 3.1というSDツールキットの事前学習モデルをベースライン (モデルP) として、下記の4種類のファインチューン版のモデル(モデルA〜D)を構築し、精度の比較を行います。 モデルP: PyAnnote Audio 3.1の事前学習モデルです モデルA: モデルPを元のビデオ会議音声(386件, 309h)でファインチューン (FT) したモデルです モデルB: モデルPをCSD(386×2[会議室とカフェ]件, 647h)でFTしたモデル。386件の会議はモデルAと同じものです モデルC: モデルPをより大規模なCSD(1,516×2件, 2,536h)でFTしたモデルです モデルD: モデルPをより大規模なCSD(1,516×2件, 2,536h)と、中国語のSDデータセット AISHELL-4 (168件, 93h)の学習セットでFTしたモデルです 上記のSDモデルP,A~Dを用いて、学習に使用されていないLSDとHAD、および中国語のSDデータセットである AISHELL-4 のテストセットをSDした時の Diarization Error Rate (DER) を下記の図に示します *1 。Diarization Error Rateは、値が小さいほど精度が良いことを示します。 各データセットに対するDiarization Error Rate(DER、エラーバーは標準偏差) ご覧の通り、Room Simulatorで生成したデータで学習した モデルC と モデルD は、既存モデル(モデルP)や、シミュレーションなしのデータで学習したモデル(モデルA)よりも、HADとLSDでのエラー率が大幅に改善していることが分かります。 また、モデルDの結果を見ると、ターゲット環境のデータ (CSD) だけでなく、中国語のデータセットなども追加で学習させることで、特定環境への適応(LSDやHADでの高精度)と、他の環境への対応力(汎化性能)を両立した、より安定して堅牢(ロバスト)なモデルになることが示唆されました。 まとめ 本研究により、Room Simulatorという技術を活用することで、「いつ、誰が話しているか」を特定するAIモデルを、低コストかつ効率的に特定の実環境へ適応させられることがわかりました。 今回は、PyRoomAcousticsという、計算量の小さな鏡像法をベースとするRoom Simulatorを用いましたが、より精密な波動音響解析などのシミュレーション手法を用いた場合に、どのように結果が変わるのかにも興味が湧きます。 今後も、SDの精度改善に取り組んでいきます。 *1 : こちらのDERは時間の誤差許容量[ms] のcollar を500ms とし、オーバーラップを無視する設定で計算されていることにご注意ください。
アバター
イベント概要 https://2025.pycon.jp/ja 公式サイトから引用 2025年、Pythonカンファレンス「PyCon JP」は、「あつまれPythonのピース」をテーマに、広島で開催されます。初の地方開催となる今回は、会場が平和記念公園内の国際会議場。平和を願い、発信し続けてきたこの地で、Pythonが大切にしてきた「多様性」や「オープンさ」を、あらためて感じられるイベントになるでしょう。 関東や遠方の方も、少し足を伸ばして、凛とした空気の平和公園で技術トークを楽しむ時間には、きっと特別な価値があります。 全国から集まる仲間たちと、コードの話も、これからの社会の話もちょっぴりしてみませんか?お好み焼きも、牡蠣も待っています。9月、広島でお会いしましょう! 日程: 2025年9月26日(金)〜27日(土) 会場: 広島国際会議場 (広島県広島市中区中島町1−5) 主催: 一般社団法人 PyCon JP Association チケット申し込みはこちらから pyconjp.connpass.com 登壇情報 Pythonだけでつながるあなたのアイディア、フロントエンドもPythonで Pythonユーザーが作成したスクリプトやデータ分析、機械学習モデルなどの成果物を「見せる」「使ってもらう」には、アプリ化というハードルがあります。 本セッションでは、Pythonユーザーが自分のコードをすぐWebで共有できるようになるための手法として、PythonだけでGUIを構築できるWebアプリフレームワーク「Flet」の仕組みと使い方を紹介します。 Fletは、裏側でPyodideを使ってWebAssemblyを活用することで、従来のWeb開発の壁を取り払ってくれています。まずはこのフレームワークを通してPiodideとWASMを深掘ります。 次に実際に使えるサンプルを通じて、「見せられるPythonアプリ」を手軽に作る方法をデモを交えて解説します。 Pythonだけで、誰かとつながれる そんな開発体験を提案します。 日時: 2025年9月26日(金) 15:00 - 15:30 登壇者: Shintaro Matsudo リンク: https://2025.pycon.jp/ja/timetable/talk/S8DUBR
アバター
はじめに 基本知識・用語の確認 1. なぜ移行をしたのか 1.1 テストの実行時間がとにかく長い 1.2 Nuxt3移行に伴うビルド&テスト環境の統一 1.3 テストコードの見直しが必要 2. Vitest を選んだ理由 2.1 手軽 2.2 フロントエンドロードマップの Testing に変化 2.3 Jest との互換性も問題なし 3. 実装 3.1 Vitest 1. Vitest をインストール 2. Vitest の設定ファイルを追加 3. Jest で記述されている箇所を、vi に変更する 4. テストで使用する環境変数の読み込み 5. エラーの解消 6. テストを成功させる 7. Jest で使っていた記述を削除する 4. 結果 まとめ はじめに はじめまして。フロントエンドエンジニアをしている伊藤と申します。 私はプロダクトの動作を保証するものとして、テストは欠かせないものだと思っています。 私が所属するチームのプロダクトでも以下のテストを行なっています。 テストフレームワークを利用したユニット/コンポーネントテスト クラウドサービスを利用したE2Eテスト QA その中で、私が一番関わりが深いのは、テストフレームワークです。現在のチームに参画した時から、チームの JavaScript テストフレームワークは Jest が使われており、長年苦楽を共にしてきました。ただ、そんな親友の Jest ともついにお別れする時がきてしまいました。 本記事では、 経緯などを踏まえて、Jest → Vitest への移行について紹介していきます。 基本知識・用語の確認 Jest JavaScript コードの品質と信頼性を確保するために、テストの作成・実行・検証・分析を一つで完結できるように設計された便利なツール Vite モダンな JavaScript/TypeScript プロジェクトの開発とビルドを、信じられないほど速く、シンプルに、そして効率的に行うための次世代ツール Vitest Vite の高速性を活かし、Jest と高い互換性を持つことで、開発者体験とテスト効率を大幅に向上させるJavaScript/TypeScript向けの新しいテスティングフレームワーク 1. なぜ移行をしたのか 1.1 テストの実行時間がとにかく長い 所属するチームでは、Git のワークフローを使用しています。特定のブランチに向けて PR が作成されるとワークフローが開始されます。そのワークフロー内で、Jest で記述されたテストを実行しています。 ただ、そのテストの完了までがとにかく長いです。平均して15分ほどかかっていました。DevUX の観点からすると、とんでもない悪影響です。 開発者の満足度低下 生産性やイテレーション速度の低下 バグ発見の遅れ コード品質への影響 1.2 Nuxt3移行に伴うビルド&テスト環境の統一 Vue2/Nuxt2 から Vue3/Nuxt3 へ移行した際、ビルドツールについても変更を行いました。 Nuxt3 では標準で Vite が採用されているため、Webpack から Vite へ切り替えています。 ただし、テスト環境は従来どおり Jest(Webpackベース)を利用しており、本番環境との間に差異が残っていました。 そこで、より実行環境に近い形でテストを行えるように、Nuxt3と親和性の高いVitestへ移行が必要でした。 上記を解決することで、環境の一貫性が保たれ、テストの信頼性も向上すると考えました。 1.3 テストコードの見直しが必要 私が参画した後もテストコードは増えていく一方でした。 どんなテスト内容か、最適なテストができているかなどは年々不透明になっていました。 潜在的な課題とはなっていましたが、テストコードを見直すことは、他の新規開発を優先してしまったため、後回しになっていました。 2. Vitest を選んだ理由 2.1 手軽 Vitest は Vite がないプロジェクトでも活用することができます。 手軽に勧められる点がポイントです。 2.2 フロントエンドロードマップの Testing に変化 フロントエンドエンジニアが学ぶべきロードマップの中に、Testing という項目があります。 おそらく Jest を勧めていた箇所が、Vitest に変わっていました。 https://roadmap.sh/frontend 2.3 Jest との互換性も問題なし 下記の表は、簡単に Jest と Vitest を比較したものです。 比較をして、Vitest への移行をしても問題ないと思いました。 (独自で調べた内容で作成したため、誤っている場合もあります) 3. 実装 3.1 Vitest 1. Vitest をインストール npm install -D vitest 2. Vitest の設定ファイルを追加 Vite を使っているプロジェクトなら、vite.config.ts に設定を記述 使っていない場合は、vitest.config.ts を新規作成 (下記は、弊社の例です。) ``` /// <reference types="vitest" /> import path from 'path'; import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import { config } from 'dotenv'; // .env.test を明示的に読み込む config({ path: path.resolve(__dirname, 'src/.env.test') }); export default defineConfig(() => { return { plugins: [vue()], resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js', '@@': `${__dirname}`, '@': `${__dirname}/src`, }, }, test: { setupFiles: ['src/tests/setup.ts'], include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], exclude: ['node_modules'], // テスト対象から除外するパターン environment: 'jsdom', globals: true, environmentOptions: { jsdom: { url: 'http://localhost/', // 必要であれば設定 }, }, cache: true, }, }; }); ``` 3. Jest で記述されている箇所を、vi に変更する 一括変換でもいいですし、AI エージェントに任せるのも手です。 4. テストで使用する環境変数の読み込み テスト用の環境変数を用意している場合は、vite.config.ts にその旨を記載します。 // .env.test を明示的に読み込む config({ path: path.resolve(__dirname, 'src/.env.test') }); 5. エラーの解消 mock を使用している箇所でエラーが出たので解消しました。 (エラー内容:mock を使うなら、一番最初の行に記述してください。) Error: [vitest] There was an error when mocking a module. If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file. Read more: https://vitest.dev/api/vi.html#vi-mock 6. テストを成功させる 7. Jest で使っていた記述を削除する jest.config.ts など、Jest 関連のものを削除します。 4. 結果 テストの実行時間の大幅な短縮ができました。 Vitest への移行と、既存の不要テストの削除による成果だと思います。 以前まで、15分かかっていたテストが5分で終了するようになりました。 おかげでリリースまでの時間も縮小することができました。 もしかすると、Vitest への移行だけでここまで縮小することはなかったかもしれませんが、 プラスの成果はあったと思います。 まとめ Jest → Vitest 移行を行なったことで一番の収穫は、既存のテストを見直す機会を得たことです。 どうしても新規開発をしていると、テストコードにまで気が回らなくなってしまいます。 その結果、放置されてしまっていて、テスト内容が適正かなどの判断が後回しになっていました。 ただ、今回の移行でテストコードの見直しをするきっかけをもらったと思います。 AI を使った開発により、テストコードでテストしたいことが実現しやすくなりました。 本記事が、皆様の関わっているプロジェクトのテストコードを見直すきっかけになれば幸いです。 これからもより良いテストコードを目指していきます。
アバター
はじめに こんにちは、RevCommでエンジニアをしている加藤と申します。先日AWS Unicorn Day Tokyo 2025に参加してきました!今回はその中で特に印象に残ったセッションをお話ししたいと思います! 会場の様子/全体の雰囲気 会場は原宿駅からほど近い東郷記念館というところでした。オフィスは渋谷にあるので朝早めにオフィスで仕事をしてから会場まで歩くことにしました。暑かったですが、青山通りを抜けて行く感じは気持ちが良かったです。 東郷記念館 外から バルコニーが綺麗 今年は AWS Summit Tokyo(幕張メッセ)や Google Cloud のイベント(Google 渋谷オフィス)にも参加しましたが、今回の会場である東郷記念館はまた違った印象でした。結婚式場としても使われるだけあって華やかで、池には鮮やかな鯉が泳ぎ、外国人観光客が写真を撮る姿も多く見られました。ちょっとした観光地のような雰囲気の中でのイベントでした。 参加セッション & 学び 今回は参加セッションの中でも特に印象的だった3つについてご紹介します。 1. “AIエージェント時代の創業” — コネクシー株式会社 コネクシー社のSlackからPRまでのワークフロー 設立わずか2025年2月のコネクシー社による、「エージェントネイティブ」な開発体制の構築事例が紹介されました。特に意識されていたのは「モバイルから誰でも開発できること」のようです。エンジニアだけでなく CEO も Slack 経由で Devin にメッセージを送り、PR まで作ってもらっているそうです。 面白かったのは、Devin への指示がどんどん短くなっていったという話です。最初は「XX という機能を作りたいです。ヘッダーは YY に配置して、ボタンは ZZ に……」と詳細に指示していたのが、日が経つにつれ「XX を追加してください」といったシンプルな命令になっていったそうです。 人間の側がAI が文脈(コンテキスト)を理解していると自然に感じてしまう心理的変化が興味深く、コンテキストを確実に伝えるためにはDevin AI の Knowledge や Playbook の機能をきちんと整備する必要があると感じました。 2. “ID 管理基盤内製化の意思決定” — 株式会社カミナシ カミナシ社によるID基盤内製化 概略構成図 一般的には IDaaS が選ばれるケースが多い中、カミナシ社はあえて ID 管理基盤の内製を選んだそうです。直前のセッション「自社ブランドの共通 ID・シングルサインオンの設計と実装 - Amazon Cognito 設計パターン -」で Cognito の全体像を紹介した後に、「Cognito を使わない」という選択が語られたのが印象的でした。AWS的にはCognitoを使わなくてもAWSの別サービスを使っているからアリなんでしょうね。 もちろん内製には RFC 準拠の開発やドメイン知識が求められ、難易度は高いものの、今回の意思決定では「役員の迅速なトップダウン判断」が大きなポイントになったそうです。ID 基盤の移行は数年単位の大規模プロジェクトになることも多いため、将来的なプロダクトビジョンに基づく判断が有効だったのだと思います。 3. “AI エージェントとはそもそも何か? - 技術背景から Amazon Bedrock AgentCore での実装まで -” — AWS Japan speakerdeck.com 最後に聴講したのは、AWS が提供する Agent 構築環境に関するセッションでした。Agent構築においてAWSが重視しているのは「差別化に繋がらない重労働(Undifferentiated Heavy Lifting)」のようです。後で調べましたが、これはAWSではよく出る言葉で「サーバーのラッキング、積み上げ、電力供給などのデータセンターの手間のかかる運用作業」が Well-Architected フレームワーク に書いてありました。この考え方は、AWS Lambdaにも引き継がれ、そして現在は、エージェントの構築にもこの考え方が活かされているようです。 特に認証、ブラウザ利用、メモリ保存といったどのエージェントでも必須になるような機能をAWS側で提供してくれることは非常にありがたいなと思いました。 まとめ AWS Unicorn Day Tokyo2025に参加してきました。技術的な話だけでなく、事業戦略や意思決定の考え方まで踏み込んだイベントでした。 やはりAWSはエージェントという新しい機能をどうAWSで実行するかという内容を含んでいました。スタートアップに参加しやすいようなシステム作り、コミュニティづくりを目指しているように感じました。 私も業務でStrand AgentsやBedrock AgentCoreを触ったりしています。今回得られた知見を活かし、自社プロダクトにどのように適用できるかをさらに検討していきたいと思います!
アバター
9/10(水)〜12(金)に東北工業大学(仙台)で開催される日本音響学会秋季研究発表会に、プリンシパルリサーチエンジニアの石塚賢吉が登壇します。会場にいらした方は、是非お立ち寄りください。 登壇日時・内容 日時 : 9/12(金)13:00〜15:00(※石塚は13:00〜14:00に対応します) 場所 : ポスター会場 タイトル : 「Room Simulator を用いたデータ拡張による Neural Speaker Diarization モデルの実環境適応」 概要 : 対面での会議録音では、話者ダイアリゼーションの際に残響を考慮する必要があります。本研究は、 残響を考慮したデータセットを効率よく構築し、その有効性を示した ものです。 イベント概要 日程 : 9/10(水)〜12(金) 会場 : 東北工業大学 八木山キャンパス(宮城県仙台市) リンク : https://acoustics.jp/annualmeeting/program/ 登壇者 石塚 賢吉 株式会社RevComm プリンシパルリサーチエンジニア 筑波大学大学院博士後期課程卒業。博士(工学)。日本HP株式会社にて通信事業者向けのシステム開発、株式会社ドワンゴで全文検索システムの開発などに従事。2019年12月株式会社RevComm入社。音声認識、音声感情認識、全文検索システムの研究開発を行なっている。 → 過去記事一覧
アバター
はじめに 生成AIの急速な発展により、エンジニアを取り巻く環境は激変しています。特に注目すべきは、Coding Agentの登場によって多くの場面で生成AIが実用的なコードを書けるようになったことです。実際、単純な機能実装からバグ修正まで、Coding Agentに任せる仕事が日々増えています。 一方で、Coding Agentを効果的に使いこなすには、エンジニア自身の高い技術力が不可欠です。適切な指示を出し、生成されたコードの品質を評価し、システム全体の整合性を保つためには、従来以上の深い理解が求められます。また、アーキテクチャ設計、技術選定、チームマネジメントなど、未だにCoding Agentには任せられない重要な業務も数多く存在しています。 さらに重要なのは、Coding Agentが人間の成長速度を大きく上回るペースで発展し続けていることです。エンジニアとして社会に貢献する活動を続けたいなら、自身もこれまで以上のスピードで成長することが不可欠になっています。 そこで本記事では、このような時代背景を踏まえ、私がエンジニアとして社会に貢献し続けるために生成AI(主にChatGPT)を活用している手法を紹介します。 1. 自分には何が足りていないか、何をやるべきかをChatGPTを使って考える 1.1 エンジニアグレードの客観的評価 まず、自分の現在地を正確に把握するために、ChatGPTにエンジニアのグレード判定を依頼しました。以下のプロンプトを使用しています。 私に質問をしながらソフトウェアエンジニアとしてのレベルを把握し、シニアになるために必要な知識や技術を整理してください。 質問は問題でも構いません。 質問は20問として、1回に1つ行なってください。似たような質問にならないようにしてください。 このプロンプトの特徴は、対話式で進行し、ChatGPTが1つずつ質問を出してくることです。1回に1つ、という指示を入れることで1回のやりとりで大量に質問してくるのを防いでます。例えば以下のような質問がありました。 「システム設計でキャッシュを導入する際に、キャッシュの有効期限(TTL)を設定する場合、どのような考慮事項がありますか?具体例を交えて説明してください。」 「データベースにインデックスを作成する際、インデックスを追加することによる利点と欠点をそれぞれ1つずつ挙げ、具体的なシナリオを用いて説明してください。」 「分散システムで 'CAP定理'(Consistency, Availability, Partition Tolerance)の3つの要素を簡単に説明し、それぞれがトレードオフとなる理由を述べてください。」 回答後、ChatGPTからは以下のような評価を受けました: あなたのスキルは、すでに中堅エンジニアとして高いレベルにあります。ただし、シニアエンジニアとして求められる「より大規模で複雑なシステムの設計」や「チーム全体を見据えた意思決定」のスキルをさらに伸ばす余地があります。 このような客観的な評価により、自分の現在地と次のステップが明確になりました。 1.2 マンダラチャートによる成長戦略の立案 次に、エンジニアリングマネージャー、ソフトウェアアーキテクト、スタッフエンジニアの3つのキャリアパスを想定したマンダラチャートの作成を依頼しました。 マンダラチャートとしたのは、必要だと思われる要素を網羅的に抽出したかったからです。 5年目のSaaS開発のソフトウェアエンジニアが、*** になるためのマンダラチャートを作成してください ※ *** の部分には、ソフトウェアアーキテクト、エンジニアリングマネージャー、スタッフエンジニアなどのキャリアパスを入れて実行します。 このプロンプトは非常にシンプルで、キャリアパスごとに別々に実行することで、それぞれに特化したマンダラチャートを生成できます。SaaS開発の経験を前提としているため、より実践的で具体的なスキルセットが提案されます。 例えばソフトウェアアーキテクトでは以下のようなチャートが生成されました。 1.3 ポジショニングマップによる現状分析 マンダラチャートの各項目を抽象化し、強み・弱みを横軸、機会の多さを縦軸にしたポジショニングマップを作成しました。また、NLP(神経言語プログラミング)の学習5段階をカスタムした成熟度の自己評価を各項目に対して行いました。 無意識的未経験(知らないしやったこともない)→ 緑色 意識的未経験(知っていてもやったことがない)→ 青色 意識的経験(知っていてやったことはある)→ 黄色 意識的有能(考えるとできる)→ 赤色 この評価をマッピング時の色に反映させることで、優先的に取り組むべき領域を視覚化しました。 3,4ヶ月くらい前に作成したのですが、青色も業務でやっていないことはないので、もっとやっていないといけないという厳し目の自己評価だったのだろうと思います。 1.4 実践への落とし込み ポジショニングマップの分析結果から、主に真ん中よりも左側にある青色の項目は比較的機会もあり伸ばす領域なのでここに注力すると良さそうなことがわかります。これによって業務では以下の2点を意識してタスクを取ったり、課題を探したりしやすくなりました。 課題が明確で、取り組めるもの x 自分が伸ばしたい領域のもの 課題がありそうだが明確ではなく、調査が必要なもの x 自分が伸ばしたい領域のもの 加えて、設計は開発と比べてたくさんやれる機会があるわけではないので、後述する Architectural Katas を始めることにも繋がりました。 2. Architectural KatasとChatGPTを使ってシステムデザインを学ぶ 2.1 Architectural Katasとは Architectural Katas( https://www.architecturalkatas.com/ )は、プログラマーがプログラマーとしての実践の機会を必要とするのと同じように、ソフトウェアアーキテクトにはソフトウェアアーキテクトとしての実践の機会が必要であるという願望から生まれたサイトであり実践方法です。社内で設計を学びたいならこういうものがある、と教えていただきました。 Architectural Katas 自体はグループで行う前提となっていますが、私は試しに ChatGPT とやってみて一定価値を感じたのでその方法を共有します。 2.2 ChatGPTによるシステムデザイン面接の実践 ChatGPTで Architectural Katas を実施する方法は、海外のBig Tech 企業でもよくあるシステムデザイン面接を参考にしています。ChatGPT を面接官としてどのように振る舞って欲しいかをプロンプトで指定します。 私はソフトウェアエンジニアとして優れたアーキテクトになるべくArchitectural Katas を活用して勉強したいです。 あなたは以下の指示に従い、ソフトウェアデザインの試験官のように振る舞って私の学習をサポートしてください。 - まず、私が Architectural Katas の設問をソフトウェアデザインの試験官であるあなたに与えます。 - 与えられた設問について曖昧な部分は、試験官であるあなたが定義する必要があります。定義をするだけで私が質問するまで公表する必要はありません。 - 私が設問について質問した場合にソフトウェアデザインの試験官として適切な返答をしてください。 - その返答を参考に、私は設問に対して設計を行い、回答を仕上げます。 - 私の回答に対してレビューを行い、良い点、更に考慮すべき点、改善案を提示してください。更に考慮すべき点、改善案については、その考慮をしたり案を出したりすることが、どれくらいのレベルのソフトウェアエンジニアに求められるか、難易度がわかるようにしてください。 このプロンプトによって、ChatGPT は問題を把握しても質問や回答があるまでは、何かをユーザーに教えるようなことはありません。ユーザーは問題を確認し、前提条件や想定されるユーザー数やリクエスト数やデータサイズなどを見積って ChatGPT に認識の確認するとズレていないか回答してくれ、設計をして提出するとフィードバックを得ることができます。 2.3 実際のイメージ オンラインでドキュメントを作成・編集・バージョン管理・印刷でき、手動・自動決済もできるサービスをつくるというような問題が出たときの、実践模様を書いていきます。 システムプロンプトを渡したあと、問題を渡します。 ChatGPT が返事をしてきます。(確認の観点を先回りして発言していますが、そういうときもあります。) 以下のように前提条件を確認すると、ChatGPTから返事が来ます。長いので割愛します。 実際に設計してみた回答を ChatGPT に送ります。 ChatGPT からフィードバックが返ってきます。 良い点も挙げてくれますが、改善すべき点もレベルと一緒に教えてくれます。 以下はフィードバックの抜粋です。 総評 スコープ内のコア機能(ドキュメント編集・バージョン管理・印刷ジョブ・決済)を 4 つのサービスに分割しており、役割も明確です。 エッジに CDN を置き、リアルタイム更新は WebSocket 専用サービスに任せる設計も理にかなっています。 初期利用者「数千 → 数百万」を見据え、キューで印刷ジョブを非同期化しているのもスケールに有効です。 ~~~ さらに考慮すべき点 & 改善案 バージョン管理の実装(Mid) S3 バージョニング+version_id を RDS に参照保存差分保存(delta/DDS)で容量削減 リアルタイム編集方式(Senior) 単独編集ならロック制御 (optimistic/pessimistic) 将来協調編集を見込むなら OT/CRDT ライブラリ採用 印刷ゲートウェイ不足(Senior) Print Job Service の下に Printer Gateway を追加:IPP over TLS、Secure Release (PIN/NFC)、ドライバー隔離 (コンテナ) 決済の信頼性 (Mid) PSP からの Webhook 受信 → 冪等キーで payment_status 更新 サービス間通信は Outbox/Polling で二重送金を防止 更に考慮すべき点 & 改善案 で知らなかったものについてはチケット化し、別でインプットしたり掘り下げることで、幅広い知識と観点を養えることを期待しています。 週1回を目安に始めて、まだ1ヶ月程度ですがそれでも知らなかった技術に出会い、自分が見落としやすい観点を見つけ、どこの知識が足りないのか、知っていると思っていて実はちゃんと知らなかったものは何かがわかるので、とてもおすすめです。 3. 英会話をChatGPTとやる 3.1 背景と課題 生成AIによるサービスが急増するなかで英語を学ぶことに時間を使うかどうかは、人によって判断が分かれると思います。私自身は、特に予定はありませんが社会に貢献し続けるために将来的に海外で働いたり大学院に進学したりを、自分のしたいタイミングでできるよう、IELTS を定期的に受験しています。IELTSは Reading、Listening、Writing、Speaking 全てのテストがあり、Speaking は自己紹介とインタビュー、スピーチ、ディスカッションという3つのパートに分かれています。現在の私のスコアはOverall 6.0ですが、Speaking だけが5.0とボトルネックになっている状況です。弊社には英語話者も在籍していますが、私が所属しているチームや関わる方々の多くは日本語話者なので、日常的に英語を話す機会はありません。 3.2 ChatGPTをIELTS試験官として活用 弊社には多言語学習・会話レッスン補助制度があるので、オンライン英会話を受けていた時期もありましたが、ChatGPTに音声による会話機能が出てから、ChatGPTに移行しました。現在は以下のようなプロンプトを利用しています。 IELTS Speaking Testの練習を行います。以下の条件に従って練習を全力で手伝ってください。 - "OK, I'm ready for {パート番号}" と入ったらそのパートの問題を1つランダムに出題してください。 - 私が英語で回答するので、それに対してAnswer(私の回答), Mistakes and Corrections, Natural Corrections, High Score Rephrasing, Additional Points, Example Answer を整理して教えてください このプロンプトによってフィードバックが6つの項目に体系化されます。 スピーキング力の向上のためには、話す内容が思いついていないのか、英語に変換できないのか、を分離するため、英語で話す以前にそもそも日本語で同じ質問をされたときに十分な回答ができるのか?ということを振り返る必要があります。 このプロンプトでは、「Additional Points」を挙げるよう指示しており、そのとき自分が思いつかなかった観点を教えてくれるので、話す内容を思いつけていないことを自然と振り返れるようになっています。 また、話す内容を思い付いているけれどもっと他の言い回しはなかったのだろうか、というときには「High Score Rephrasing」で、同じ内容をより高いスコアが期待できる表現に変換してくれるため、自分では思いつかなかった表現を教えてくれます。 3.3 実際のイメージ 技術進歩がコミュニケーション方法にもたらした影響について聞かれたときの模様を書いていきます チャット形式になっていますが、実際は音声でやりとりしています。 回答を終えるとChatGPT からフィードバックが返ってきます。 実際に私が話した内容について、三人称単数形になっていないことや、より自然な言い回しを教えてくれています。加えて私が言及していない内容に触れ、それらを含めた回答を作成してくれています。 以下はフィードバックの抜粋です。 Mistakes and Corrections Instead of "technology enable us," it should be "technology enables us." "Communicate remotely" is great, but you can simplify "in the past, we could not communicate with other people remotely" to "in the past, we couldn't communicate over long distances." Instead of "bring our content of talking or texting," you can say "transmit our conversations and messages." ~~ Additional Points You could also mention how technology has introduced new forms of communication, like video calls, social media, and instant messaging. ~~ Example Answer “In my opinion, the most significant impact of technology on communication is the removal of physical barriers. In the past, people had to be in close proximity to interact, but today we can instantly transmit our conversations and messages across the globe. Furthermore, innovations like instant messaging, social media platforms, and video calls have transformed how we connect: we can chat in real time via apps, share life updates and multimedia on social networks, or hold face-to-face conversations with someone thousands of miles away. These developments have made communication more seamless, diverse, and engaging than ever before.” 3.4 実践結果 ChatGPTから添削された文章を音読して空で言えるようにする、というのを始めてからボキャブラリーが増えたと思います。優先度が下がって数ヶ月やらない期間があったりしながら、1年くらいこの方法でやっています。定期的に、といいましたが1年弱はIELTSを受けていない気がするので、そろそろ受けようかと思います。 まとめ 本記事では、エンジニアとして社会に貢献し続けるための、私の実践例を紹介しました。 おそらく似たようなアプローチを取っている方も多いでしょうし、中にはこれらの手法をアプリケーション化して運用している方もいらっしゃることと思います。 生成AIを業務に取り入れることはもちろん、うまく使って継続的に成長し続けることが重要だと思うので、ぜひ何かの参考になれば幸いです。
アバター
RevComm で音声処理を中心に研究開発を担当している加藤集平です。 私はADHD(注意欠陥・多動症)という障害を抱えています 。ADHDを持つ人は日常生活でさまざまな困難に直面するもので、もちろん仕事をしていく上でも困難があります(障害を持たない人と同じやり方では困難に直面します)。私も例に漏れずさまざまな困難に直面していますが、 2022年12月に本ブログで公開した記事 では、当時それらの困難にどのように対処しようとしていたのかを紹介しました。また、弊社の働き方の特徴であるフルフレックス・フルリモート環境が及ぼす影響についても取り上げました。 本記事では、 前回の記事の公開から2年半が経過し状況が変化したことを踏まえ 、私が現在直面している困難とそれに対する対処、弊社の働き方の特徴であるフルフレックス・フルリモート環境が及ぼす影響について 改めて整理してお伝えします 。 内容に前回の記事と重複するものが多くありますが、この記事単体で読んでいただけるようにしたいと思ってのことです(ADHDの方は特に、複数の記事を行き来するのはつらいかと思います)。ご容赦くださいませ。 加藤集平(かとう しゅうへい) シニアリサーチエンジニア。RevCommには2019年にジョインし、音声処理を中心とした研究開発を担当。ADHDと付き合いつつ業務に取り組む2児の父。 個人ウェブサイト X → 過去記事一覧 本記事を読むにあたっての注意 私はADHDを専門とする医師でもその他の専門家でもありません。 ADHDに関する正確な情報は、専門家の発信をご参照ください 。 ADHDの症状(困難に直面するポイントあるいは本人の特性)は人によって異なる ことが知られています。また、同じ症状に対して同じ対処が有効とは限りません。本記事で取り上げるのは私の症状と私が実践している対処法であり、 万人に通用するものではありません 。 ADHDの診断は医師のみが行うことができます 。自己判断はかえって困難を増大させるおそれがあります(例えば、症状が似た違う病気かもしれません)。他人を勝手にADHDだと断定することについても同様に、本人および周囲の困難を増大させるおそれがあります。 ADHDとは ADHD(注意欠陥・多動症)とは、精神障害のうち発達障害に分類されるものの一つです。発達障害とは、生まれつきみられる脳の働き方の違いにより、幼児のうちから行動面や情緒面に特徴がある状態です。発達障害の中でもADHDは不注意・多動性・衝動性の3症状を主な特徴としており、それらの症状の影響で日常生活・学業・仕事などに様々な困難が生じることがあります。かつては子供だけに見られる病気と考えられていましたが、現在では大人になっても症状が継続する場合があることが知られています。 私とADHD 私がADHDと診断されたのは、2017年(30歳頃)のことでした。当時は前年に発症した強迫性障害という病気の治療のために心療内科に通っており、通院・治療の過程でADHDであることが発覚しました。 強迫性障害とあわせて障害者手帳の交付を受けています(現在はカード型が選択できるようになりました)。 思えば物心ついた頃から忘れ物や物をなくすのは日常茶飯事で、部屋は常に散らかっており、コツコツ勉強することは決してなく、学校のテストではよく不注意で失点をしていました。 大人になり仕事を始めてからは、順序立てて仕事を処理することが苦手で締切に間に合わなかったり、他人に出した指示をすっかり忘れたり、単調な作業ですぐに寝てしまったり、体調に波があるために毎日8時間パフォーマンスを出し続けることが難しかったりして、仕事の遂行に支障をきたしていました。また、かつての勤務先では毎日オフィスに出社していたのですが、電話番をすることや周囲の話し声(雑音)が苦痛で頭がいっぱいになったりといった困難もありました。 診断を受けてからは、定期的に通院の上、服薬および日常生活の中での治療を続けています。 この状況は2年半前と特に変わっていません 。 フルフレックス・フルリモート環境における恩恵と困難 ADHDを持つ人にとって、弊社のようなフルフレックス・フルリモート環境は適しているのでしょうか?私の場合は恩恵のほうが大きく勝りますが、フルフレックス・フルリモートならではの、オフィス出社にはない困難も感じています。これらの恩恵と困難を紹介します。 恩恵 体調の波を吸収しやすい(フルフレックス) ADHDを持つ人すべてに当てはまるわけではないと思いますが、私は体調に比較的大きな波があります。つまり調子のいい日と悪い日の仕事のパフォーマンスの差が大きくなるおそれがあります。 現在は2年半前と比べてパフォーマンスの差は小さくなりましたが、依然として多少の波はあります 。 フルフレックスの制度下では体調に合わせて比較的柔軟に勤務時間(長さおよび時間帯)の調整ができます。当然、打合せやプロジェクトの進行状況などの制約条件があるので完全に自由に調整できるわけではありませんが、それでも 毎日絶対に決まった時間に仕事をしなければならない状況よりは安心感が違います 。 静かな環境で仕事ができる(フルリモート) 自宅や家族構成などの諸条件に左右されますが、オフィスよりも静かな環境を用意することができる場合があります(私は用意できています)。私の場合は雑音が多い環境が苦手なので、 静かな環境は集中力を高めるのに役立っています 。 困難 自主的にやる気を管理する必要がある(フルフレックス・フルリモート) フルリモート環境では、オフィスのように衆人環視の中で仕事をするわけではありません。人の目がない環境だとどうしても怠けやすくなります。しかし怠けすぎると、仕事の成果が出ず問題になります。 逆に、やる気に満ちあふれている時には過剰な長時間労働をするおそれもあります。フルフレックスの制度下では(法令の範囲内で)極端な時間の使い方をすることも不可能ではありませんが、健康の観点や、組織の一員として周囲と協調しつつ働く観点からは望ましくないでしょう。 ADHDを持つ人にはやる気のある時とない時の差が激しい人が少なくありませんが、やる気のない時に最低限のやる気を出すことと、やる気に満ちあふれている時に働きすぎないようにする工夫は、体調を整えつつ安定したパフォーマンスを出す上で重要だと考えています。 私の場合は、先述したように子育ての関係で ある程度決まった時間に働くことになっており、2年半前と比較して体調とパフォーマンスをより安定させることに繋がっていると感じています 。 家事などの私生活と仕事のバランスを意識して取る必要がある(フルフレックス・フルリモート) フルフレックス・フルリモート環境では、仕事中にいつでも私用を挟むことができます。特に在宅勤務の場合は、仕事の合間に家事をすることは珍しくないでしょう。 ところが、ADHDを持つ人には一度集中したら他のタスクになかなか移り難い傾向のある人が少なくありません(過集中)。つまり、家事を始めたらいつまでも仕事に戻れなかったり、逆に仕事に熱中して家事が疎かになったりすることがあります。 仕事に戻れないことは当然問題になりますし、家事が疎かになることも私生活においては問題になりえます。 フルフレックス・フルリモートとは関係のない一般的な困難 膨大なタスクを適切に管理する 前回の記事を公開した2年半前と異なり、現在の私は多数のプロジェクトに少しずつ関わるという働き方をしています。このような働き方においては、 自ずとタスクの数は増え、しかもそれらを同時並行でこなす 必要があります。ADHDを持つ人にとって、多くのタスクを同時並行でこなすことは一般に苦手なことの一つだと思います。私も苦手なので、対処する必要があります。 私が困難に対処している方法の例(★は前回の記事以降に新たに始めたこと) 「自主的にやる気を管理する必要がある」に対して 仕事前に着替える 在宅勤務では、打合せがなければパジャマのままでも仕事をすることが可能です。打合せがあっても、下半身はパジャマのままでもバレません。しかし、私の場合は気持ちを仕事に切り替えるために、仕事前に必ずパジャマから着替えることにしています。オフィスに出社していれば通勤時間で気持ちを切り替える人も多いかと思いますが、在宅勤務は通勤時間がないので代わりにしっかり着替えることにしています。パジャマよりも寝心地が悪いので、安易な昼寝を防止する効果も期待できます。 筆者の仕事着の例。下は白いですがジーンズです。 専用の仕事部屋で仕事をする 誰もが実践できる方法ではありませんが、私はほぼ仕事専用の部屋を用意しています。私生活の場と空間を分けることで、仕事に対するやる気を出しやすくなります。やる気に乏しい日でも、机に座ってしまえば仕事ができることは珍しくありません。 特にADHDの人には、トリガーが大事であることは経験的にご理解いただけるかと思います 。 ★毎日ある程度決まった時間に働く フルフレックスの精神に反すると感じられる方もいらっしゃるかもしれませんが、 自分で決めた時間でいいので毎日ある程度時間を決めて働くことは、体調の安定に資すると実感しています 。 私の場合は、2年半前と違って子供が保育園に通っている時間に大半の仕事を終える必要があり、自ずと毎日ある程度決まった時間に働くことになっています。自ら望んで決まった時間に働いているというよりは、そうせざるを得なかったので決まった時間に働いているだけなのですが、結果としてはいい方向に作用していると感じています。 ★原則として規定の労働時間しか働かない フルフレックスでも、1か月あたりの規定の労働時間や、それを超えた場合の残業という概念はあります。毎日ある程度決まった時間に働き、原則として1か月あたりの規定の労働時間の範囲内で働くことで、 締切効果 (締切直前だけ頑張れるアレです)が働きます。 私の場合は、子供が保育園に通っている時間に大半の仕事を終える必要があるため、自ずと労働時間に制限がかかります。この 制限をうまく活かして、集中力を発揮する ことに成功しています。 ★ポモドーロ・テクニックを活用する ポモドーロ・テクニックとは、25分間の作業と5分間の休憩を繰り返すことで、集中力を維持して生産性を向上させるやり方です。 定期的なリズムと短時間の集中が、やる気をうまく発揮し、かつ疲れすぎないことに繋がっていると感じています 。 2年半前には既にリマインダーで過集中を防いでいましたが、対処がより洗練された方法になった感じです。 「家事などの私生活と仕事のバランスを意識して取る必要がある」に対して 私生活の時間はカレンダーをブロックしてしまう 前回の記事では、昼食を取り損ねるために昼食の時間 (12:00 – 13:00) のカレンダーをブロックしていました。 現在は昼食が自然と取れるようになったのでその時間はブロックしていませんが、 起きてから子供を保育園に送るまでの時間と、迎えの後に子供が寝るまでの時間はブロック しています。こうすることで、その時間はしっかりと家事・育児に集中することができます。 「膨大なタスクを適切に管理する必要がある」に対して ★タスクをリマインダーで管理する さまざまなベストプラクティスがあると思いますが、私が現在使っているのは広く知られたやり方の一つであるGetting Things Done (GTD) という方法です。単にタスクを列挙するだけでは特にADHDを持つ人にはつらいかと思いますが、GTDではタスクをシステマティックに管理することができます。タスクを管理する媒体は人それぞれですが、私はiPhoneのリマインダーを利用しています。 詳細は書籍や解説記事をご覧いただければと思いますが、個人的にはGTDを採用することで認知負荷が劇的に減り、 目の前のタスクに安心して集中できる ようになりました。これにより、より多くのタスクを、より少ない疲労で完了させることに成功しています。 2年半前はやっていたが、現在はやらなくなったこと 朝起きたら布団を畳む 仕事中に寝ることがほとんどなくなったので、やめました(行儀としては畳むべきかもしれません)。 コンテンツブロッカーを使う コンテンツブロッカーを使わなくてもネットサーフィンをすることが減ったので、やめました。 適度に打合せを入れる 状況の変化により意識しなくても打合せが入るようになったので、わざわざ入れることはやめました。 労働時間をトラッキングする タスク管理で十分に仕事が終わるようになったので、やめました。 スマートスピーカーに頼る(タイマー・アラーム) 2年半前は「洗濯をしたのに、つい仕事に熱中して何時間も干し忘れる」ことへの対策でタイマーを活用していましたが、自動で乾燥まで行ってくれる洗濯機に買い替えたので、やめました。 おわりに 2年半前とは生活も業務内容も随分と変わり、それに伴って困難や対処も変わりました。 全体としては、日々工夫を重ねることで、より上手に対処できるようになったと感じています 。 なお、以上の困難や工夫は私にとって一部であり、他にも仕事・私生活を問わず様々な困難に対してさまざまな工夫を日々行っています。また、周囲の方々の支援なくして良好な社会生活を送ることはできません。 改めて家族やRevCommの仲間をはじめとする周囲の方々に感謝いたします 。
アバター
RevCommで音声処理を中心とした研究開発を担当している加藤集平です。昨年3月に第二子が生まれて、1年間の育児休業を取得しました。私は男性ですが、男性の育児休業取得率・取得期間ともにここ数年急速に伸びている実感があります。しかし、1年間の育児休業を取得する例はまだまだ少ないように思います。本記事では、 男性として実際に1年間の育児休業を過ごした経験から、正直どうだったのか について共有します。 加藤集平(かとう しゅうへい) シニアリサーチエンジニア。RevCommには2019年にジョインし、音声処理を中心とした研究開発を担当。ADHDと付き合いつつ業務に取り組む2児の父。 個人ウェブサイト X → 過去記事一覧 なぜ1年間の育児休業を取得することにしたのか? 第一子のときは1か月間だった 第一子もRevComm在籍中に生まれたのですが、その際は里帰り出産への同行+1か月間の育児休業(生後2か月目の1か月間)という形でした。なお、妻は産後休暇と、子供が1歳になるまでの育児休業を取得しました。 ところが、1か月間の育児休業は、 家庭内環境の激変による部屋の模様替えと片付け(出産・育児で物が増えたので片付けてスペースを空ける必要がありました) いただいた出産祝の管理(内祝を返す必要があるので表にしていました) 来客対応(第一子ということもあり多かったのです) に追われているうちに、あっという間に終わってしまいました。妻が体力の回復と子供の世話に集中するのに役に立ったとは思いますが、 自ら子育てをしたか?と言われると疑問符の残る状況 でした。 さらに、生まれたばかりの子供というのは、昼夜を問わず「寝る→起きる→泣く→授乳→寝る→…」を3時間〜4時間ごとに繰り返します。私は昼間業務があるので夜はまとめて寝かせてもらっていましたが、妻は細切れにしか睡眠が取れないので毎日大変です。個人差は大きいと思いますが、第一子の場合、夜に比較的まとまった睡眠を取るようになったのは生後4か月目頃からでした。 第二子は当初3か月間の予定だったが、1年間に延長した というわけで、第二子の誕生に際して、 当初は3か月間 の予定で育児休業に入りました。なお、妻は今回も産後休暇と、子供が1歳になるまでの育児休業です。私が育児休業中は、2人で休業していることになります。 ところが、実際にやってみると3か月間もやはりあっという間に過ぎていきます。上に第一子がいますから子育ての難易度としては上がっており、なかなか心身の調子が整いません。育児休業も3か月目に入る頃、そのように思い悩んでいました。色々考えましたが、だったら思い切って1年間休んだほうが、自分にとっても家族にとっても会社にとっても最終的には利益になるのではないかと考え、当初の計画を延長し、 1年間の取得 とすることにしました。 1年間何をしていたのか? 子供の成長フェーズによって世話の負担は変わる 男性が1年間育児休業を取得する例は現状では少ないので、何をしていたのか気になると思います。まず言えるのは、 生後1年間の子供の世話の負担は一様ではない ということです。あくまで私の子育ての経験 (n=2) の話ではありますが、おおむね以下のようでありました。 1〜3か月: とにかく忙しくて眠い およそ3か月目までは、前述のとおり子供はしょっちゅう寝たり起きたりして、授乳間隔も短い状態です。養育者全員(うちの場合は夫婦)が毎回付き合う必要はありませんが、睡眠はどうしても浅くなりがちです。また、 公的手続や儀礼が多数あり 、それらを確実にこなすにはかなりの労力が必要です。実際、第一子の時は完璧にやり終えたのですが、第二子の時は健康保険の手続を失念して少々困ったことになりました。今回は第二子ということで、赤ちゃん返りする上の子のケアも欠かせません。とにかく忙しくて眠い日が続きました。 4〜6か月: 空き時間が最も多い 生後4か月頃になると、(うちの場合は)昼夜のリズムがだんだんとできてきて授乳間隔も伸び、したがって大人も比較的寝られるようになります。子供はというと、首はすわったけれど、ハイハイはできないような時期です。つまり、その場から動くことはできません。 子の安全を常に監視する必要はありますが、危険は比較的少ない状態 です。うちの場合は、子供が息をしているか感知するセンサーを布団の下に仕込んでいました。そうすると、少しばかり空き時間ができます。この空き時間に心身を休めることも重要ですが、余裕があれば他のことをすることができます。 7〜9か月: 動き始めて目が離せなくなる 個人差が大きいですが、この時期にハイハイ(ずり這いを含む)を始める子が多いと思われます。 ハイハイを始めると、子供が危険に遭遇する確率はグッと上がります 。ずっと目を離さないのも疲れるので実際にはベビーサークルに入れたりしていましたが、目を離せない時間が増えるのは間違いなく、空き時間はその分減ります。 10〜12か月: 復職準備 スムーズに復職したければ、どうしても準備が必要 です。最低限、すっかり変わってしまった生活リズムを元に戻すことは欠かせません。子育てに何とか慣れてきたところではありますが、最後の3か月は、だんだんと復職に向けた動きをすることになるでしょう。 私がしていたこと 上記のように、育児休業といえど100%の時間を子供の世話に費やすわけではありません(うちは夫婦で休業していたので、少なくともそうです)。むしろ、業務と子育ての両方で忙しい普段よりもまとまった時間が取れることもあるでしょう。私は以下のようなことをしていました。 業務に関する学び直し 私は業務で機械学習(特に深層学習)を扱いますが、深層学習が普及したのは私が大学院を卒業した後であり、体系的に学ぶ機会がありませんでした。深層学習はそれ以前の機械学習とは根本的に性質の異なる面があり、いわゆるアンラーニングが必要な状況でもありました。 そこで、育休の4〜6か月目を中心に、オンライン動画の講座を利用して、深層学習について体系的に学ぶことにしました。ついでに、機会学習のための数学・機械学習全般・敵対的生成ネットワーク (GAN)・自然言語処理・デジタル信号処理についても同様に学び直しを行いました。 さらに、復職準備の頃には、かねてより伸ばしたいと思っていたソフトスキルの講座を受け始めました(現在継続中)。 なお、育児休業中に育児以外のことをするのは賛否あるかと思います。個人的には、育児休業法の理念に照らしても、空き時間を活用してよりよい復職のための準備を行うことは、決して悪いことではないと考えています。 育児・介護休業法 第三条の2 子の養育又は家族の介護を行うための休業をする労働者は、その休業後における就業を円滑に行うことができるよう必要な努力をするようにしなければならない。 実家への長い帰省 いつもお盆と正月に短期間滞在するだけの実家ですが、普段より長めの帰省を行いました(私はついでに学会の聴講をしていたのですが…)。うちの場合は夫婦ともに実家が遠方であり、貴重な機会となったと考えています。 復職はスムーズだったか?復職して思うことは? 復職は比較的スムーズだった 復職準備には前述のように3か月間を充てることができたので、余裕を持って準備をすることができました。うちの場合は4月1日にいきなり第二子を保育園に預ける生活が始まるわけでもちろん混乱はありましたが、何とか乗り切ることができました。なお、私は子供の誕生日の前日(3月)の復職、妻は慣れ保育(慣らし保育)が終わった4月中旬の復職と復職時期をずらすことで、一つ一つの変化を小さく抑える工夫をしました。 また、1年間業務から離れていましたが、子育てに忙しい中でも比較的余裕を持って心身を整えることができ、慌てずに復職することができました。もっとも、2人の子供を育てる中で、多少のことでは動じない心がいつの間にか身についていたのかもしれません。 復職して思うことはたくさんある 1年間の育児休業を取得するという選択は、 1年間業務に従事しないという選択 でもあります。嫌々業務に取り組んでいるなら別ですが、業務を離れるということは多少なりともつらい思いがありますし、悔しいものでもあります。 比較的スムーズに復職することができて、現状では育児休業に入る前よりもむしろよいパフォーマンスを出せていると実感しています。これは、業務を長期間離れることでよい意味で心身がリセットされたこと、空き時間に行った学び直しを応用できていること、子育てを通じてよりタフになったことなどが関係していると考えています。 一方で、もし業務を離れていなければ、自分なりの貢献ができたのではないかと思う事象に遭遇することもあります。離れていた時間が戻ってくることはありませんし、よいパフォーマンスで今から貢献するしかないのですが、申し訳なくさみしく思う気持ちはゼロではありません。 長期の育児休業がハードスキルとソフトスキルに与える影響について 真摯に子育てに取り組んだことでソフトスキルが伸びた あくまで私の個人的な経験でしかありませんが、 真摯に子育てに取り組んできたことは、ソフトスキルを伸ばすことに役に立った と考えています。私の場合は妻と共同で行っているので、妻との間で子育ての方針から細かな点まで合意形成をする必要があります。たとえ夫や妻がいなくても、子育ては一人でできるものではなく、どうしても周りの人や社会的な支援を受けながら行うことになります。夫や妻がいれば、互いに助け合うことになります。どのような支援を受けるか、あるいはどのように助け合うか、 主体的に考え調整する 必要があります。 このような作業は時に面倒で大きな労力を要しましたが、真摯に取り組んだことで、特に合意形成や調整といった面でソフトスキルが伸びたと考えています。前述のように別途ソフトスキルの講座は受けていますが、それだけでは足りない実践力が身につきました。さらに言うと、 子の成長を見守るというのも大事な経験 でした。無理やり成長させようというのは不可能です。粘り強く見守る力も身についたのかもしれません。 ハードスキルはキャッチアップが必要 一方で、 ハードスキルについてはキャッチアップする必要 がありました。私の取り組んでいる機械学習(特に深層学習)の世界は、まさに日進月歩。本当に1年間離れていれば浦島太郎です。絶え間なくキャッチアップする必要はありませんが、どこかでキャッチアップする必要はあります。離れるのは育児休業だから仕方ありませんし、浦島太郎になることは別に悪いことではないと思いますが、元に戻るためにはキャッチアップが必要です。ただ、個人的にはハードスキルは勉強すればいい話でソフトスキルを伸ばすほうが難しいと考えているので、 長期の育児休業を取ったことは(少なくとも結果的には)よかった と考えています。 さいごに: 自分の選択を尊重しよう 育児休業を取得するという選択も、取得しない選択も、どちらも大きな決断を伴います。それが1年間という長期間であればなおさらです。何かを選択するということは、他の選択肢を諦めるということであり、何かを得る代わりに何かを失うということです。 私は第二子の誕生にあたり1年間の育児休業を取得する選択をしました。会社から一人が一年間離れるというのは、決して小さなことではなく、さまざまな人に影響を与える選択でした。しかし、(選択をした当時としては)最善を尽くした選択だったと考えており、結果として現在はよい状態とパフォーマンスで業務にあたることができています。もちろん、万人に1年間の育児休業を勧めているわけではなく、他の方は他の選択をされるかもしれません。育児休業に限った話ではありませんが、大きな選択であればあるほど 最善を尽くして選択を行い、少なくとも自分自身がその選択を尊重される ことを願います。
アバター
はじめに 昨今、パッケージなどのエコシステムをターゲットとしたサプライチェーン攻撃が増加しています。 各種プログラミング言語向けのパッケージマネージャーやレジストリにおいては、インストールするパッケージのバージョンを固定したり、チェックサムを検証したりすることにより、サプライチェーン攻撃被害のリスクを軽減する仕組みが導入されています。 もちろんGitHub Actionsにおいても、サードパーティー製のワークフローを利用する場合に、サプライチェーン攻撃の被害を受けるリスクが生じますが、このような仕組みを導入するには少々作業が必要になります。 そこで本記事では、pinactを利用して簡単にGitHub Actionsにおけるサプライチェーン攻撃被害のリスクを軽減する方法を紹介します。なお、本記事で紹介する内容は、弊社で最近実施されたものです。 なぜ実施したのか? 弊社の一部リポジトリで利用している tj-actions/changed-files において、サプライチェーン攻撃が発生しました: www.stepsecurity.io 具体的には、 tj-actions/changed-files の運用のためのボットで利用していたPATが流出し、それを悪用して攻撃者による悪意のあるコミットが紛れ込み、各タグなども改竄されてしまったようです。 時差の関係もあって運よく被害は発生しなかったのですが、仮に被害が発生した際の影響は大きなものになりえます。今後のリスク軽減のために、以下で紹介する対策を実施することにしました。 実施したこと pinact の導入 pinact とは? GitHub Actionsのワークフローにおける各種依存アクションのバージョンを固定してくれるCLIツールです。 github.com 使い方 pinact はHomebrewなどで導入可能です。 $ brew install pinact 導入したら、以下を実行します。 $ pinact run すると、リポジトリ内の各種ワークフローにおける依存アクションを検出し、以下のようにバージョンの定義を書き換えてくれます。 steps: - - uses: actions/setup-node@v4 + - uses: actions/setup-node@cdca7365b2dadb8aad0a33bc7601856ffabcc48e # v4.3.0 with: node-version: '22' - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 pinactの実行結果 このように、 pinact によってサードパーティー製ワークフローのコミットハッシュによる参照を強制することで、意図せず改竄されてしまったバージョンのワークフローが実行されるリスクを軽減することができます。 pinact の導入に関する選択肢 今回、 pinact の導入に当たって実現したいことは以下の内容です。 CI (GitHub Actions) で pinact run --check を実行したい。 pinact run --check を実行すると、GitHub Actionsのワークフローにおいてバージョンが固定されていない依存アクションを検出してくれます。 目的はサプライチェーン攻撃へのリスクを低下させることなので、できるだけ安全な方法で pinact を導入したい。 その上で、 pinact を導入する方法としては以下のあたりが選択肢として考えられそうです。 Homebrew メリット 公式から Homebrew/actions が提供されており、GitHub Actionsからの利用が容易である。 pinactによって推奨されるインストール方法の一つである。 使い慣れているユーザーが比較的多いと思われる。 デメリット インストールするツールのバージョンを固定できない。 aqua メリット 公式から aquaproj/aqua-installer が提供されており、GitHub Actionsからの利用が容易である。 pinactによって推奨されるインストール方法の一つである。 依存ツールのバージョンの固定が可能である(後述)。 aqua-checksums.json によるチェックサムの管理・検証が可能である。 デメリット Homebrewや後述する mise と比較すると、まだ使用例は多くないと思われる。 mise メリット 作者によって jdx/mise-action が提供されており、GitHub Actionsからの利用が容易である。 aqua バックエンド を利用すれば pinact を導入可能である。 aqua と同様に、依存ツールのバージョンの固定が可能である。 今回導入予定の pinact 以外に、Node.jsなどのさまざまなランタイムのバージョン管理もできる。 デメリット aquaバックエンドは、実験的サポートの段階である (※)。 今回の目的とメリットを踏まえると、 aqua か mise がよさそうです。 mise の aqua バックエンドはまだ実験的サポート(※) であったことと、 aqua-checksums.json によるチェックサム管理の仕組みがあることなどから、本記事では aqua を試してみることにしました。 ※ ⚠️ 記事の執筆を開始した当初は、 mise の aqua バックエンドはまだ実験的サポートの段階でしたが、本記事の公開時点ではすでに実験的という表記は削除されています ( af36cfd )。 今回は aqua を採用しましたが、 mise も選択肢として有望だと思います。 aqua とは CLIツール向けのパッケージマネージャーで、サプライチェーン攻撃に対する対策が強く意識されているのが特徴です。 slsa-verifier によりパッケージが検証される( SLSA はサプライチェーン攻撃への保護を目的としたフレームワーク・仕様です) チェックサムが検証される プロジェクトごとに依存ツールのバージョンが固定される 導入方法 ローカルに導入する際は、Homebrewや公式のインストーラーなどで導入可能です。 $ brew install aqua GitHub Actionsで aqua を利用したい場合は、 aquaproj/aqua-installer を使用します。 - uses : aquaproj/aqua-installer@e2d0136abcf70b7a2f6f505720640750557c4b33 # v3.1.1 with : aqua_version : 'v2.46.0' skip_install_aqua : "true" - uses : actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with : path : '~/.local/share/aquaproj-aqua' key : v2-aqua-installer-${{runner.os}}-${{runner.arch}}-${{hashFiles('aqua.yaml')}} restore-keys : | v2-aqua-installer-${{runner.os}}-${{runner.arch}}- aqua の設定 まず、設定ファイルである aqua.yaml を生成します。 $ aqua init (推奨) aqua.yaml を生成したら、まずチェックサムの検証を有効化することを推奨します。 # aqua.yaml checksum : # See https://github.com/aquaproj/aquaproj.github.io/blob/4709985f3f10c1c257fc812d9f791ab595cad266/docs/reference/config/checksum.md#require_checksum for details enabled : true require_checksum : true # 以下は必要に応じて調整します (`aqua update-checksum`の実行時に該当の環境向けのチェックサムが登録されます) supported_envs : - darwin - linux/amd64 この aqua.yaml と後述する aqua-checksums.json は、バージョン管理に含めます。 パッケージの追加 aqua g -i <パッケージ名> でパッケージを追加できます( aqua のパッケージレジストリは こちら にあります)。 # 例) pinactを追加 $ aqua g -i suzuki-shunsuke/pinact # 例) actionlintを追加 $ aqua g -i rhysd/actionlint すると、 aqua.yaml にパッケージの定義が追加されます。 packages : - name : suzuki-shunsuke/pinact@v2.0.4 - name : rhysd/actionlint@v1.7.7 aqua.yaml で定義されている各種パッケージをインストールするには、下記コマンドを実行します。 $ aqua i ( 推奨 ) aqua.yaml でチェックサムの検証を有効化している場合、依存パッケージの追加や更新などを行なった際に、 aqua update-checksum で aqua-checksums.json を更新しておく必要があります。 $ aqua update-checksum actionlint を導入する pinact に加えて、 actionlint もGitHub Actionsにおけるセキュリティを改善する上で有用なツールです。今回はあわせて導入します( actionlint についてはすでにWeb上に情報が十分にあるため、詳細は割愛します)。 actionlint は aqua でも導入可能です。 $ aqua g -i rhysd/actionlint pinact と actionlint をGitHub Actionsで実行する aqua によって pinact と actionlint を導入し、GitHub Actionsによって実行を自動化します。 name : Lint workflows on : push : branches : - main paths : - '.github/**/*.yml' - '.github/**/*.yaml' pull_request : branches : - main paths : - '.github/**/*.yml' - '.github/**/*.yaml' jobs : lint : name : Lint workflows runs-on : ubuntu-latest steps : - uses : actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - uses : aquaproj/aqua-installer@e2d0136abcf70b7a2f6f505720640750557c4b33 # v3.1.1 with : aqua_version : 'v2.46.0' skip_install_aqua : "true" - uses : actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 with : path : '~/.local/share/aquaproj-aqua' key : v2-aqua-installer-${{runner.os}}-${{runner.arch}}-${{hashFiles('aqua.yaml')}} restore-keys : | v2-aqua-installer-${{runner.os}}-${{runner.arch}}- - name : Run pinact run : pinact run --check - name : Run actionlint run : actionlint 一連の対策によってGitHub Actionsに関するサプライチェーン攻撃へのリスクを軽減することが期待されます。 おわりに 以下のブログ記事では、今回紹介したものよりもさらに踏み込んだ対策が紹介されています(ちなみにこの記事は、今回紹介した aqua や pinact の作者の方によって書かれています)。 zenn.dev 参考になる記事だと思いますので、興味がありましたらぜひ上記の記事もご覧ください。
アバター
2025年4月12日 (土)に開催される「ふりかえりカンファレンス2025」にバックエンドエンジニアの大谷紗良が登壇します。 イベント概要 名称: ふりかえりカンファレンス2025 日程: 2025年4月12日 (土) 9:00〜18:00 会場: 株式会社 フィードフォース confengine.com 登壇情報 大人数会議のカオス化を防ぐふりかえりフレームワークを考えてみた 「15人以上で1年規模のプロジェクトのふりかえりを1時間でしよう!!!!!!(えっ」 大人数での会議はカオス化しがちだと思います。 基本的にはまず適切な人数にできないかを考えるのが良いと思いますが、 大人数の会議で抱えている問題は本当に大人数の会議だけの問題でしょうか? ファシリテーターの腕に自信がなくてもフレームワークの工夫でカオス化を乗り切る一例を紹介します。 登壇者: 大谷紗良 株式会社RevComm バックエンドエンジニア 日時: 2025年4月12日 (土) 16:35〜16:40 参加申し込み 参加申し込みやイベントの詳細などについては下記ページから確認いただけます。オンラインでの参加も可能なため奮ってご参加ください。 retrospective.connpass.com
アバター
はじめに Recoilからの移行先について Jotai について 移行の方針 1. MiiTel Phoneにおいて使用されているRecoilのAPIを一通り洗い出して、それぞれのAPIにおけるJotaiへの移行方法を調査する 2. 依存関係としてjotaiパッケージを追加する 3. MiiTel Phoneにおける特定の機能において、RecoilからJotaiへの移行を実施する 4. 検証環境で様子を見る 5. 問題のない機能から順次、リリースを実施する 6. 3〜5のステップを繰り返す 7. 一通り移行が完了したら、依存関係からrecoilパッケージを削除する Recoil と Jotai の各APIの対応と移行方法について jotai/utilsモジュールについて atom() useRecoilState() useSetRecoilState() useRecoilValue() useResetRecoilState() selector() 非同期selector useRecoilValueLoadable() useRecoilCallback() useRecoilRefresher_UNSTABLE() Atom Effects atomFamily()/selectorFamily() 悩みどころ/ハマったところ atomFamily()/selectorFamily()の移行について RecoilとJotaiにおける更新タイミングの微妙な差異について おわりに 参考 はじめに 今年のはじめにRecoilのGitHubリポジトリがアーカイブされ、話題になりました。 > This repository has been archived by the owner on Jan 1, 2025. It is now read-only. https://t.co/uiv2W0Hd04 — Daishi Kato (@dai_shi) 2025年1月5日 github.com 弊社で提供している MiiTel Phone においても、フロントエンドの状態管理のためにRecoilを採用していました。 Recoilのメンテナンス停止に伴い、今後、バグや脆弱性などに関するリスクが増加してしまう可能性があることや、React v19や周辺ライブラリのアップデートなどに当たって問題となる可能性もあります。そのため、RecoilからJotaiへの移行を実施することにしました。 Recoilからの移行先について Recoilから別のライブラリへの移行については、すでに他の企業でも事例がありそうです。 speakerdeck.com 弊社においては以下の理由から、Recoilからの移行先としてはJotaiが最も有力な選択肢と判断し、移行先として選択しました。 JotaiはAPIや思想がRecoilに近く、移行コストを抑えやすい RevCommにおける他のサービスにおいてすでにJotaiの利用実績があり、十分に安定して動作することが期待できる JotaiのリポジトリやJotaiの作者である dai-shi さんの ブログ などにおいてアウトプットが活発に行われており、今後、採用事例などがより増えることが期待できる Jotaiはドキュメントが充実しており、またRecoilと比較するとライブラリのサイズも大幅に小さいです。調べたいことや気になることなどがあった際に、ドキュメントやソースコードなどから調査が行いやすいと考えられます。 Jotai について Jotaiが開発された経緯や概要については、作者である dai-shi さんによる解説記事が公開されています。これらの記事を参照するのがおすすめです。 zenn.dev zenn.dev zenn.dev 移行の方針 まず、RecoilからJotaiへの移行にあたって、ビッグバンリリースは避けたいと考えていました。できる限り、すでに存在する機能への影響やバグの発生を最小限に抑えつつ、段階的に移行が行えると理想的です。そこで、以下のような方針で移行を進めていくことにしました。 MiiTel Phoneにおいて使用されているRecoilのAPIを一通り洗い出して、それぞれのAPIにおけるJotaiへの移行方法を調査する 依存関係として jotai パッケージを追加する MiiTel Phoneにおける特定の機能において、RecoilからJotaiへの移行を実施する 検証環境で様子を見る 問題のない機能から順次、リリースを実施する 3〜5のステップを繰り返す 一通りの機能で移行が完了したら、依存関係から recoil パッケージを削除する 1. MiiTel Phoneにおいて使用されているRecoilのAPIを一通り洗い出して、それぞれのAPIにおけるJotaiへの移行方法を調査する まずは、Jotaiへの移行が現実的に可能であることや具体的な移行方針を決めやすくするために、MiiTel PhoneにおけるRecoilの各APIの使用方法を洗い出して、それらのAPIのJotaiへの移行方法を調査しました。幸いなことに、Jotaiが提供する jotai/utils モジュール (詳細は後述します) において、Recoilが提供する機能はほとんどカバーされていることがわかりました。調査を通して懸念や移行方法などは概ね把握できたため、実際に移行を進めていくことにしました。 2. 依存関係として jotai パッケージを追加する RecoilからJotaiへの移行に当たり、重要度や影響度合いの低い機能から優先して段階的に移行が行えると理想的です。幸いなことに、JotaiはRecoilと比較してフットプリントがかなり小さいです。そこで、移行期間中は jotai パッケージと recoil パッケージがプロジェクトに共存した状態で移行を進めていくことにしました。 recoil パッケージについては、一通り移行が済んでから削除します。 3. MiiTel Phoneにおける特定の機能において、RecoilからJotaiへの移行を実施する 重要度や影響が低めの機能から優先して、順次、RecoilからJotaiへの移行を実施します。MiiTel Phoneでは Qase というサービスを使って手動のテストケースを管理しています。そこで、Qaseによってテストがしやすい単位ごとにプルリクエストを分割して、各機能におけるRecoilを使用したコードをJotaiへ段階的に移行していきました。 また、もしAtom Effectsや selector などのRecoilにおける高度な機能を使用している箇所については、移行に先立ってユニットテストを用意しておくとより安全に移行が行えます。 React Testing Library の renderHook() を使用して該当の atom / selector もしくはそれらを利用するカスタムフックに対してテストを記述しておくと、Jotaiへ移行する際のテストの書き換えをできる限り抑えられて良いと思います (下記の例だと、 RecoilRoot と useRecoilState() をそれぞれJotaiの Provider と useAtom() へ置き換えるだけで移行できるはずです) import { renderHook } from '@testing-library/react' ; import { RecoilRoot, useRecoilState } from 'recoil' ; import { act } from 'react' ; describe ( 'preferencesState' , () => { afterEach (() => localStorage. clear ()); it ( 'persists state to localStorage' , () => { const { result } = renderHook( () => { const [ preferences , setPreferences ] = useRecoilState(preferencesState); return { preferences , setPreferences } ; } , { wrapper : RecoilRoot, } , ); expect (localStorage. getItem ( 'preferences' )).toBe( null ); act(() => result. current .setPreferences( { theme : 'dark' } )); expect (result. current .preferences).toEqual( { theme : 'dark' } ); expect (localStorage. getItem ( 'preferences' )).toBe( JSON . stringify ( { theme : 'dark' } )); } ); } ); 4. 検証環境で様子を見る 今回のRecoilからJotaiへの移行に当たって、新機能の開発や要望への対応などはストップせずに、それらのタスクと並行しながら進めました。RecoilからJotaiへの移行を実施した機能については、すぐにはリリースをせずに一週間ほど検証環境にデプロイをして様子を見ることにしました。 5. 問題のない機能から順次、リリースを実施する 検証環境で様子を見て特に問題がなさそうであれば、他の機能や修正などと合わせて少しずつJotaiへ移行したコードをリリースしていきました。 6. 3〜5のステップを繰り返す 一通り移行が完了するまで、関連した機能ごとにRecoilからJotaiへの移行を行い、少しずつ段階的にリリースを進めていきます。重要度や影響度の高い機能については移行を後回しにして、最後にまとめて移行をすることにしました。 7. 一通り移行が完了したら、依存関係から recoil パッケージを削除する すべてのRecoilのコードをJotaiへ移行し終えたら、ようやく recoil パッケージを削除できます。今回の移行に当たって段階的に移行を進めていたことや、RecoilとJotaiは全体的に思想やAPIがよく似ていて移行が行いやすかったこともあり、特に障害が発生することもなく無事に移行をすることができました。 Recoil と Jotai の各APIの対応と移行方法について Recoil の各APIごとに、Jotaiへの移行方法について紹介いたします。 jotai/utils モジュールについて Recoilが提供する高度なAPIの多くは jotai/utils モジュールによってカバーされています。この記事でも jotai/utils モジュールから提供されているAPIをいくつか紹介しますが、紹介していない機能もまだまだあります。 jotai/utils モジュールは atom の活用方法の観点からもとても参考になるため、一度、内容を調べてみるのも良いかもしれません。 atom() atom の移行は単純で、基本的には key を削除して、デフォルト値を atom() の引数に指定するよう書き換えることで移行できます。 - import { atom } from 'recoil'; + import { atom } from 'jotai'; - export const isLoadingState = atom<boolean>({ - key: 'users/isLoading', - default: false, - }); + export const isLoadingState = atom(false); ただし、 useResetRecoilState() を使用している atom についてはこの方法では移行できず、後述する atomWithReset() を使用するとよいです。 useRecoilState() Recoilの useRecoilState() はJotaiの useAtom() へそのまま置き換えることができます。 + import { useAtom } from 'jotai'; - import { useRecoilState } from 'recoil'; ... - const [counter, setCounter] = useRecoilState(counterState); + const [counter, setCounter] = useAtom(counterState); useSetRecoilState() Recoilの useSetRecoilState() はJotaiの useSetAtom() へそのまま置き換えることができます。 + import { useSetAtom } from 'jotai'; - import { useSetRecoilState } from 'recoil'; ... - const setIsLoadinge = useSetRecoilState(isLoadingState); + const setIsLoading = useSetAtom(isLoadingState); useRecoilValue() Recoilの useRecoilValue() はJotaiの useAtomValue() へそのまま置き換えることができます。 + import { useAtomValue } from 'jotai'; - import { useRecoilValue } from 'recoil'; ... - const isLoading = useRecoilValue(isLoadingtate); + const isLoading = useAtomValue(isLoadingState); useResetRecoilState() useResetRecoilState() を使用した atom をJotaiへ移行するには、 jotai/utils モジュールによって提供される atomWithReset() を使う必要があります。 - import { atom } from 'recoil'; + import { atomWithReset } from 'jotai/utils'; - export const isLoadingState = atom<boolean>({ - key: 'users/isLoading', - default: false, - }); + export const isLoadingState = atomWithReset(false); atomWithReset() によって定義された atom は、 jotai/utils の useResetAtom() によってデフォルト値へのリセットが可能です。 + import { useResetAtom } from 'jotai/utils'; - import { useResetRecoilState } from 'recoil'; ... - const resetIsLoading = useResetRecoilState(isLoadingState); + const resetIsLoading = useResetAtom(isLoadingState); selector() Recoilの selector については、Jotaiにおいては derived atom によって同様のことが実現できます。例えば以下のような selector があったとします: import { atom, selector } from 'recoil' ; const countState = atom( { key : 'count' , default : 0 , } ); const isEvenState = selector( { key : 'isEven' , get : ( { get } ) => get(countState) % 2 === 0 , } ); const doubledCountState = selector( { key : 'doubledCount' , get : ( { get } ) => get(countState) * 2 , } ); この場合、Jotaiでは以下のようにして同じことが実現できます: import { atom } from 'jotai' ; const countState = atom< number >( 0 ); const isEvenState = atom< boolean >( ( get ) => get(countState) % 2 === 0 , ); const doubledCountState = atom< number >( ( get ) => get(countState) * 2 , ); 非同期 selector Recoilの非同期 selector については、 非同期Atom を作成することで同様のことが実現できます: // Recoilの非同期selector import { selector } from 'recoil' ; export const myProfileState = selector< MyProfile >( { key : 'myProfile' , get : async () => { const profile = await client.getMyProfile(); return profile; } , } ); 以下のように atom() に Promise を返却する関数を渡すことで、同様のことが実現できます: // Jotaiの非同期atom import { atom } from 'jotai' ; export const myProfileState = atom< Promise < MyProfile >>( async () => { const profile = await client.getMyProfile(); return profile; } , ); useRecoilValueLoadable() Recoilの useRecoilValueLoadable() は非同期 selector に関する状態を問い合わせるためのAPIです: const loadable = useRecoilValueLoadable< MyProfile >(myProfileState); switch (loadable. state ) { case 'loading' : return < Loading /> ; case 'hasError' : return < Error error = { loadable.contents } /> case 'hasValue' : return < Profile profile = { loadable.contents } /> ; } Jotaiにおいて同様のことを実現したい場合、まず jotai/utils で提供されている loadable() というAPIによって非同期 atom をラップします: import { loadable } from 'jotai/utils' ; import { atom } from 'jotai' ; export const myProfileState = atom< Promise < MyProfile >>( async () => { const profile = await client.getMyProfile(); return profile; } , ); export const myProfileLoadableState = loadable(myProfileState); loadable() によって返却された atom に対して useAtomValue() を呼ぶことで、 useRecoilValueLoadable() とほぼ同様のことが実現できます: const loadable = useAtomValue(myProfileLoadableState); switch (loadable. state ) { case 'loading' : return < Loading /> ; case 'hasError' : return < Error error = { loadable. error } /> ; case 'hasData' : return < Profile profile = { loadable.data } /> ; } useRecoilCallback() Recoilの useRecoilCallback() によって、状態を柔軟に操作することができます: const runTaskIfNeeded = useRecoilCallback( ( { snapshot } ) => async ( taskId : TaskId ) => { const isTaskInProgress = await snapshot.getPromise(isTaskInProgressState); if (isTaskInProgress) return ; snapshot. set (isTaskInProgressState, true ); try { await runTask(taskId); } finally { snapshot. set (isTaskInProgressState, false ); } } , [ runTask ] , ); useRecoilCallback() は jotai/utils モジュールから提供される useAtomCallback() に置き換えることができます: import { useAtomCallback } from 'jotai/utils' ; // ... const runTaskIfNeeded = useAtomCallback( useCallback( async ( get , set , taskId : TaskId ) => { const isTaskInProgress = get(isTaskInProgressState); if (isTaskInProgress) return ; set(isTaskInProgressState, true ); try { await runTask(taskId); } finally { set(isTaskInProgressState, true ); } } , [ runTask ] ), ); 注意点として、Jotaiの公式ドキュメントにも記載されていますが、 useAtomCallback() に渡す関数は、基本的に上記のように useCallback() を適用しておく必要があります ( https://github.com/pmndrs/jotai/blob/v2.12.2/docs/utilities/callback.mdx ) もし、 useRecoilCallback() の引数として渡されるオブジェクト ( CallbackInterface )の refresh 関数に依存している場合は、次に紹介する方法へ移行する必要があります。 useRecoilRefresher_UNSTABLE() Recoilの useRecoilRefresher_UNSTABLE() は非同期 selector を再評価したい場合に利用できます。 const refresh = useRecoilRefresher_UNSTABLE(myProfileState); Jotaiで同様のことが実現したい場合は、まず jotai/utils モジュールで提供される atomWithRefresh() を使用して atom を作成します: import { atomWithRefresh } from 'jotai/utils' ; export const myProfileState = atomWithRefresh< Promise < MyProfile >>( async () => { const profile = await client.getMyProfile(); return profile; } , ); そして、この atom に対して useSetAtom() を呼ぶことで、 useRecoilRefresher_UNSTABLE() と同等のことが実現できます: const refresh = useSetAtom(myProfileState); Atom Effects JotaiにはAtom Effectsに相当する機能はありません。しかし、 atom を2つ用意するなどの工夫をすることで、Atom Effectsと同様のことが実現できます。 例えば、以下のように状態の更新時にAtom Effectsを活用してロギングを行なっている atom があったとします: import { atom } from 'recoil' ; export const countState = atom< number >( { key : 'count' , default : 0 , effects : [ ( { onSet } ) => { onSet(( newValue ) => { logger. info ( 'countState has been updated to %d' , newValue); } ); } , ] , } ); この場合、Jotaiにおいては2つの atom を組み合わせることで同様のことが実現できます。このように2つ以上の atom を組み合わせて複雑なことを実現するパターンは jotai/utils モジュールの内部においても頻繁に利用されています。 import { atom } from 'jotai' ; const baseAtom = atom< number >( 0 ); export const countState = atom< number , [ number ] , void >( ( get ) => get(baseAtom), ( get , set , newValue : number ): void => { set(baseAtom, newValue); logger. info ( 'countState has been updated to %d' , newValue); } , ); 他にも、Jotaiの jotai/utils モジュールでは atom の状態を localStorage へ同期してくれる atomWithStorage などのAPIも提供されています。このようなAPIを活用することで、RecoilにおいてAtom Effectsを利用していたコードを置き換えることも可能です。 atomFamily() / selectorFamily() 注意: ここでは jotai/utils の atomFamily() を使用した例を紹介しますが、後述するように jotai/utils の atomFamily() はユースケースによってはメモリリークを引き起こす可能性があるため、適切なタイミングでクリーンアップする必要があります。 import { atomFamily } from 'recoil' ; export const taskState = atomFamily< Task , string >( { key : 'task' , default : ( id ) => ( { id , state : 'todo' } ), } ); jotai/utils モジュールから atomFamily() が提供されており、概ね同じような用途で使用できます: import { atom } from 'jotai' ; import { atomFamily } from 'jotai/utils' ; export const taskState = atomFamily(( id : string ) => { return atom< Task >( { id , state : 'todo' } ); } ); また、 selectorFamily() についても似たような方法で移行ができます: import { selectorFamily } from 'recoil' ; export const tasksByProjectIdState = selectorFamily< string | undefined , Array < Task >>( { key : 'tasksByProjectId' , get : ( projectId : string ) => async ( { get } ) => { const filter = get(tasksFilterState); const tasks = await fetchTasksByProjectIdAndFilter(projectId, filter); return tasks; } , } ); Jotaiにおいては、 jotai/utils モジュールから提供される atomFamily() と非同期 atom を併用することで、概ね同じことが実現できます: import { atom } from 'jotai' ; import { atomFamily } from 'jotai/utils' ; export const tasksByProjectIdState = atomFamily( async ( projectId : string ) => { return atom< Array < Task >>( ( get ) => { const filter = get(tasksFilterState); const tasks = await fetchTasksByProjectIdAndFilter(projectId, filter); return tasks; } , ); } , ); atomFamily() はデフォルトでパラメーターの比較を同値性に基づいて行います。そのため、パラメーターとしてプリミティブ値ではなくオブジェクトを指定したい場合は、 atomFamily() の第2引数にオブジェクト同士の深い比較を行う関数を指定する必要があります。 Jotaiの公式ドキュメントでは fast-deep-equal を使用した例が掲載されています。 github.com 悩みどころ/ハマったところ atomFamily() / selectorFamily() の移行について 先ほども紹介しましたが、Jotaiが提供する jotai/utils モジュールには atomFamily() というAPIがあります。これは名前が示す通り、Recoilの atomFamily() とよく似た振る舞いをしてくれます。 しかし、一つ注意点があります。Jotaiの公式ドキュメントにおいても記載されていますが、 atomFamily() は内部において作成された atom の一覧を Map を用いて管理しています。この Map で保持されている atom の一覧は、該当の atom が unmount されたとしても破棄されることはないため、ユースケースによっては意図せぬメモリリークが発生してしまう可能性があります。 github.com このメモリリークへの対策としては、以下のいずれかが考えられると思います: AtomFamily#remove を用いて、不要になった atom を削除する Recoilからの移行に当たり、 atomFamily() の使用をやめる 1. AtomFamily#remove を用いて、不要になった atom を削除する Jotaiの atomFamily() が返却する AtomFamily オブジェクトは remove というメソッドを提供しています。 atomFamily() の内部では Map を使ってパラメーターと作成された atom の紐付けを管理しています。 github.com AtomFamily#remove メソッドにパラメーターを指定することで、 Map から指定されたパラメーターのエントリーを削除することができます。適切なタイミングで AtomFamily#remove を呼ぶことで、 Map に無制限にエントリーが残り続けてしまう問題を回避できます。 AtomFamily#getParams メソッドと併用することで、例えば、 atomFamily() が内部にキャッシュするエントリー数に制限を掛けることなどもできそうです。 また、 AtomFamily オブジェクトには setShouldRemove というメソッドもあります。このメソッドには、 atom の作成日時 及び atomFamily() に渡されたパラメーターの2つの値を引数として受け取り、 boolean を戻り値として返却する関数を指定します。この関数が true を返却した場合、 atomFamily() の内部で管理されている Map から該当のパラメーターに対応するエントリーが削除されます。古くなったパラメーターに紐づく atom を削除したいケースにおいて役立ちます。 MiiTel Phoneにおいては、できる限り移行のコストを軽減することや、移行に当たって意図せぬリグレッションなどを防止することを優先して、この方法を採用しました。しかし、Jotaiの使い方としては、この方法よりも次に紹介する方法の方がより理想的なのではないかと思っています。 2. Recoilからの移行に当たり、 atomFamily の使用をやめる Jotaiにおいて、 atom() から返却される値の実体はプレーンなオブジェクトです github.com Jotaiの Store はこのプレーンなオブジェクトから状態へのマッピングを WeakMap によって管理しています。 公式ドキュメントでも言及されているように、 useMemo() などとの併用は必要ですが、Jotaiの atom はコンポーネントのレンダリングフェーズにおいても作成することが可能です。この性質をうまく活用すると、 atomFamily() を使用せずに同様のことをより直感的に実現することも可能そうです。 github.com github.com MiiTel Phoneにおいても、徐々にこの方式への移行を検討していきたいです。 RecoilとJotaiにおける更新タイミングの微妙な差異について RecoilからJotaiへ移行するに当たって、微妙なタイミングのずれから useEffect が意図したタイミングで発火せずに不整合が起きてしまうバグに遭遇しました。 しっかりとした調査ができているわけではないので自信はないですが、Recoilは useSyncExternalStore を使っているようで、それが原因で再レンダリングなどのタイミングが微妙にJotaiとは異なっている可能性があるのではないかと推測しています。 github.com おわりに Recoilはとても便利なライブラリであり、MiiTel Phoneでもたくさん活用していました。そのため、メンテナンスが停止されてしまい残念には思いましたが、これほどの規模や需要を持つライブラリをメンテナンスし続けることは実際には非常に大変なことなのではないかと思いました。 今回、移行先として選択したJotaiは、全体的にとてもシンプルで使い勝手の良いライブラリだと思いました。ドキュメントも充実しており学習も行いやすく、とても良いライブラリです。Recoilのメンテナンス停止に伴い、今後さらに人気が増すのではないかと思います。 参考 github.com blog.logrocket.com
アバター
概要 こんにちは、RevCommのエンジニア、加藤(涼)です。今回はMiitelでAWS CognitoでSAML/OIDC SSOを汎用化した件についてお話ししようと思います。 背景 MiitelではOIDCの認証プロトコルかつ、GoogleとMicrosoft Azureのプロバイダーを用いたSSOのみにしか対応していませんでした。しかし今回、お客様からのご要望に伴いSAMLプロトコルやその他のプロバイダーに対応することとなりました。まず各用語について確認していきたいと思います。 OIDC とは OIDC (OpenID Connect)は、OAuth 2.0をベースにした認証プロトコルです。OAuth 2.0は RFC 6749 にて規定されています。 OIDCの主な特徴は以下の通りです: OAuth 2.0の認可フローに加えて、IDトークン(JWT)を使用したユーザー認証情報のやり取り Claim(ユーザー属性情報)の標準化された取得方法を提供 Authorization Code Flow、Implicit Flow、Hybrid Flowなどの認証フローが規定されており、用途に応じて適切なフローを選択します。 SAML とは SAML(Security Assertion Markup Language)は、XMLベースの標準規格で、組織間でユーザー認証情報を安全に交換するためのプロトコルです。主にエンタープライズ環境での Single Sign-On (SSO) に使用されます。SAMLは RFC 7522 で規定されています。 SAMLの主な特徴は以下の通りです: XMLベースのメッセージフォーマットを使用し、セキュリティアサーションを交換 IdP(Identity Provider)とSP(Service Provider)の間で認証情報を安全に伝送 SAMLでは、ユーザーがサービスにアクセスする際、IdPが認証を行い、認証結果をSPに対してXML形式のアサーションとして送信します。これにより、ユーザーは一度の認証で複数のサービスにアクセスすることが可能になります。 AWS Cognito とは AWS Cognitoは、AWSが提供するユーザー認証・認可サービスです。Webアプリケーションやモバイルアプリケーションにおけるユーザー管理、認証、アクセス制御を実装することができます。 Miitelではユーザー管理にCognitoを利用しています。SAML/OIDCプロバイダーをユーザープールに設定することができるため、今回は自前で実装せず、その機能をメインで使うことにしました。 構成 before 課題を再確認します。 GoogleとMicrosoft Azure のプロバイダーにしか対応していない。(DBやAPIでEnumでの管理) OIDC プロトコルのみ ユーザーがMiitel管理者にSSO設定依頼をする必要があった。 これらの問題の影響でSAMLでのご要望に応えられなかったり、SSOの設定に時間がかかり、ヒューマンエラーが発生することもありました。 After 上記の課題から ユーザのSSO設定フローを変更 SAMLの設定を可能に GoogleとMicrosoft Azure以外のプロバイダーをサポート するよう変更しました。 変更後のイメージはこのような感じです。 ユーザーはMiitel Admin上でSSOを自由に設定できるようになりました。 OIDC/SAMLおよびプロバイダーの種類に制限がなくなりました。 開発時の注意点 AWS Cognitoの1ユーザーにリンクされたID数は5つまで 開発時に陥ったエラーです。Cognitoでは1ユーザーに紐づくIDプロバイダーの数が5つまでに設定されています。 これはSSOログインをする度にユーザー属性の identities にログインのプロバイダーが登録されていき、6つ目のプロバイダーではログインしようとするとできないようになっています。 実際6以上のプロバイダーを使うユーザーはほぼほぼいないのですが、開発時には何個も登録するためこのクォータに引っかかりました。 上記の解決方法ですが aws cognito-idp admin-disable-provider-for-user を使うことによりユーザーとSSOの連携を解除できます。 create/update-identity-providerのProviderDetailsオプションが複雑 ユーザープールにidentity providerを設定するには create-identity-provider / update-identity-provider で可能です。 ただ、SAML, OIDCでパラメータが大きく異なります。 ProviderDetails というオプションがあるのですが、この中にSAMLとOIDCの詳細を全て追加します。OIDCはスネークケースのキー名なのに対し、SAMLはパスカルケースになっています。 https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/create-identity-provider.html —saml-provider-details / —oidc-provider-details に名前を分けてほしいですね。 まとめ 今回はMiitelでOIDC/SAMLプロトコルの両方に対応し、プロバイダーも複数選べるようになった件についてお話ししました。 認証方法については「ログイン・パスワード」・「Google」というのは良くみますが、SAMLプロトコルを用いたSSOにしか対応していないお客様も少なくありません。本記事が参考になれば幸いです。 以上、アカウントチームより、加藤がお話しさせていただきました。 参考 https://auth0.com/intro-to-iam/saml-vs-openid-connect-oidc https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-pools-saml-idp.html https://docs.aws.amazon.com/cli/latest/reference/cognito-idp/admin-disable-provider-for-user.html https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/quotas.html#resource-quotas
アバター
2025年1月21日(火)に開催されたML@Loft #16にリサーチエンジニアの石塚が登壇しました。 今回はイベントの振り返りとして登壇資料と登壇者の感想を紹介します。 ml-loft.connpass.com 登壇振り返り 発表タイトル: トーク解析AI MiiTelの音声処理について 発表スライド: https://speakerdeck.com/ken57/aws-yin-sheng-ji-pan-moderu-tokujie-xi-ai-miitelnoyin-sheng-chu-li-nituite 発表者: 石塚賢吉(→ 過去記事一覧 ) 登壇者の感想 ML@Loft#16音声基盤モデルのイベントでの発表は三件で、第一発表者の株式会社レアゾン・ホールディングスの末永様からは音声認識処理の基礎的な話や、商用利用可能な高精度音声認識モデルを含めたプロダクト「ReazonSpeech」を活用した音声認識の実践的な方法についての講演がありました。そして、第二発表者のKotoba Technologies, Inc.の笠井様からは、OpenAIの音声認識モデルWhisperの高速化や、TextToSpeech、同時通訳システムに関する講演がありました。 双方とも非常に興味深いお話であったと思います。 最後に、株式会社RevCommの私から、トーク解析AI MiiTelの音声処理について講演をさせていただきました。 発表後は3つのグループに分かれて議論を行いました。 主に話者ダイヤリゼーション機能や音声感情認識機能の実応用について、興味深い議論ができたと思います。 総じて、非常に有意義な会であったと思っています。 今後も、折を見てこのような会に参加し、社外の方々とも意見交換をさせていただきたいです。
アバター
はじめに 2025年01月21日(火)に開催される「ML@Loft #16 音声基盤モデル」にRevCommプリンシパルリサーチエンジニアの石塚 賢吉が登壇します。 イベント概要 ml-loft.connpass.com 名称: ML@Loft #16 音声基盤モデル 日程: 2025年01月21日 (火) 会場: AWS Startup Loft Tokyo 主催: アマゾンウェブサービスジャパン合同会社 登壇者 石塚 賢吉 株式会社RevComm プリンシパルリサーチエンジニア 筑波大学大学院博士後期課程卒業。博士(工学)。日本HP株式会社にて通信事業者向けのシステム開発、株式会社ドワンゴで全文検索システムの開発などに従事。2019年12月株式会社RevComm入社。音声認識、音声感情認識、全文検索システムの研究開発を行なっている。 → 過去記事一覧 参加登録 参加登録やイベントの詳細などについてはconnpassページより確認いただけます。奮ってご参加ください。 ml-loft.connpass.com
アバター
皆さんこんにちは。RevComm の CTO の平村 ( id:hiratake55 , @hiratake55 ) です。今年もあと数日となりました。この記事では、2024 年の RevComm の開発チームの振り返りを行いたいと思います。 この記事は、 RevComm Advent Calendar 2024 の 25 日目の記事です。 1 月: 組織体制変更 1 月には、エンジニア組織の組織変更を行いました。2023 年 12 月まではマトリックス型の組織を採用し、フロントエンドやサーバサイド、インフラ、モバイルなど、それぞれの技術スタックの専門性を活かしながら各開発プロジェクトに所属して開発を進める組織形態でした。 しかし主力の MiiTel Phone に加え、MiiTel Meetings (オンライン会議解析) や MiiTel RecPod (対面商談解析) など製品が増えてきたこともあり、プロダクト単位の開発組織への変更を行い、よりわかりやすい組織体制に変更しました。新しい組織体制ではプロダクト別の組織に加えて、組織横断で最適化を行う CTO 室で構成されています。 2 月: 開発者向けサイト MiiTel Developers を発表 開発者向けサイトの MiiTel Developers を発表しました。2023 年には、Incoming Webhook や Outgoing Webhook を開発者向けにリリースし、MiiTel にデータを登録したり、外部のサービスに連携することが容易になりました。 MiiTel Developers は、このような機能を開発する開発者向けにチュートリアルや API ドキュメントを整備することで、Developer Friendly な製品へ前進しました。 MiiTel Developers 日本語版: https://developers.miitel.com/ MiiTel Developers 英語版: https://developers.en.miitel.com/ www.revcomm.co.jp 3 月: MiiTel Phone Mobile バージョン 3 をリリース MiiTel Phone Mobile バージョン 3 をリリースしました。バージョン 3 では、プラットフォームを Flutter に変更するため、全てのコードを書き直しました。約 1 年間にわたって技術検証と開発を進め、これまで数多く寄せられていた多数の要望にも併せて対応しました。 MiiTel Phone Mobile は、バックグラウンドやロック画面での処理など、VoIP アプリならではの苦労もありましたが、4 名のチームでリリースを成功しました。 4 月: 会話コーチング機能のリリース 「会話コーチング機能」は、生成 AI を活用して、ユーザーの会話の傾向が他のユーザーと比較してどのような状態にあるのかを、システムが自然な文章で会話の改善点を学校の通知表のように届ける機能です。 これまでは、ダッシュボードを操作して傾向を把握する必要がありましたが、このリリースにより自分自身の会話の改善点やよくできている点を簡単に具体的に把握することができるようになりました。 www.revcomm.co.jp 5 月: SMS 機能のリリース MiiTel の SMS 機能は通話終了後や通話中に SMS をユーザーが取引先のお客様に送信できる機能です。また、コールセンターでオペレーターにつながるまでに時間を要する場合や、オペレータの数が限られている場合に、SMS を送信しお客様をお待たせしないようにするための機能です。 この機能の開発は多くのメンバーが関わりました。音声通信システムを開発するチーム、ブラウザ上の通話アプリを開発するチーム、応対履歴データを管理するチーム、料金計算を担当するチーム、キャリアから回線の仕入れを行うチームなど社内の多数のチームがコラボレーションすることで、短期間でリリースを成功させました。 www.revcomm.co.jp 7 月: MiiTel Scan To Call のリリース MiiTel Scan To Call をリリースし、記者発表会を開催しました。MiiTel Scan To Call は、QR コードをスキャンするだけで、通話料無料、アプリのインストールを必要とせず、モバイルブラウザから電話による通話が可能な革新的なサービスです。また、どの媒体を見て発信したかをトラッキングできるため、これまで困難とされていた電話の広告効果測定が可能になりました。 www.revcomm.co.jp 8 月: 全社オフサイトミーティング RevComm では約 260 名の社員がフルリモート・フルフレックスで業務にあたっています。オフサイトミーティングでは、フルリモート・フルフレックス勤務のレブコムにとって、年に1度、全社員が通常業務から離れ、部署を超えたコミュニケーションを取ることのできる貴重な機会です。オフサイトミーティングでは、CEO の會田、経営企画の鈴木、そして CTO の私から、経営や事業に関するプレゼンテーションを行った後、懇親会で交流を深めました。 note.com 10 月: 経団連へ入会 経団連へ入会しました。スタートアップが経団連に入会したというニュースには驚いたメンバーも多く、私から入会目的や狙いを説明しました。 入会の理由としては、国内外の経済動向や政策に関する情報収集を強化し、事業成長を加速させること。また、日本の経済界や各業界のリーダーと連携し、AI や音声テクノロジーを中心としたイノベーションを推進すること。経団連というと、重厚長大系のお堅い企業群というイメージがありますが、ここに新しい風を吹かせることがスタートアップに期待されていること、グローバルで日本を代表して活躍する企業を目指していくことです。 www.revcomm.co.jp 10 月: インドネシア出張 インドネシアのジョグジャカルタで開催された PyCon APAC 2024 で RevComm から 3 名のエンジニアのプロポーザルが採択され、プレゼンテーションを行うためインドネシアへ出張しました。 また、ジャカルタにあるインドネシア子会社の RevComm Indonesia のオフィスにも訪問し、同時にユーザー会のイベントや顧客訪問のため出張で滞在していたプロダクトマネージャーや私も合流して交流会を開催しました。 RevComm Indonesia では販売とサポートを行い、日本のメンバーとはリモートでインドネシアチームと新機能の企画やお客様との対応についてディスカッションを行っていますが、実際に現地にインドネシアの社会課題やカルチャー、テクノロジーの浸透度を肌で感じることができ、モチベーションが高まりました。 note.com note.com 11月: 総務大臣賞受賞 「第18回 ASPIC クラウドアワード 2024」で表彰を受けた全約 130 社中、最高位の総務大臣賞を受賞し、阿達総務副大臣より表彰を受けました。受賞の背景として、ユーザーが最新のテクノロジーを日々のビジネスに活用できるようになっている点、ユーザー数や導入企業数の増加度合い、海外進出をしている点を高く評価されました。 表彰の概要は 総務省のサイト にも掲載されました。 www.revcomm.co.jp 12 月: re:Invent 参加 米国ラスベガスで開催された AWS の re:Invent に 3 名のエンジニアが最新技術の調査のため参加しました。生成 AI やデータベースの新機能、機械学習モデルを効率的に学習・推論するための仕組みについて、これまでリリースされていたものの知らなかった機能や、量子コンピューターやブロックチェーン、スポーツにおける IT の活用、IoT など業務では扱うことない知識を得ることができ、新サービスや既存サービスの効率化を考える上でのインスピレーションになりました。 まとめ 2025 年も魅力的な新サービス、新機能のリリースを予定しています。世界で活用される MiiTel のサービスの開発に興味のある方は、ぜひ応募をお待ちしております。
アバター
こんにちは。Corporate Engineeringチーム所属の @mottake3 と申します。本記事は RevComm Advent Calendar 2024 の 24 日目の記事です。 はじめに ツールの説明 実装手順 slack appのインストールとtokenの取得 tokenをSecret Managerに登録 アプリケーションコードの説明 Cloud Runへのデプロイ Event Subscriptionsの設定 Slack Channelへインテグレーションの追加 終わりに 参考 はじめに Slack などのテキストコミュニケーションにおいて、伝えたいことを丁寧な言葉遣いでスムーズに作文するのが難しいことがあります。特に音声入力などでメッセージを作成する場合、丁寧な表現にしようとすると発話数が増えてしまい、入力に時間がかかってしまいます。ChatGPT などを活用して文章を校正している方もいるかと思いますが、複数のアプリ間で作業を切り替えるのは少々手間がかかります。そこでSlack上で画面を切り替えることなく、より簡単に自然で丁寧な文章を Slack に投稿できるようなプチツールをSlack BoltとVertex AIを用いて作成してみました。 注意事項 本記事のコードはあくまでサンプルですので参考程度に御覧ください。 セキュリティなどの考慮についても同様になります。 ツールの説明 特定のスタンプを押すと、Vertex AI上のLLMに文章を校正するプロンプトが投げられ、その結果が新規メッセージとして投稿されます。スタンプを外すと編集前のメッセージは削除されます。編集前と編集後のメッセージを見比べて問題があれば手動で微修正をすることを想定してます。スレッド内のメッセージの場合はそのスレッド内で新規メッセージが作成されます。 アーキテクチャの略図は以下のようになります。長くなってしまうので本記事では赤枠の部分の実装を目標にご説明しようとおもいます。 ※その他の部分に関しては別記事として追ってどこかに掲載しようと考えてます。 アーキテクチャ略図 Cloud Run 実行環境です。利用しないときは0スケールさせてコストを節約することを想定しています。 Slack Bolt Slack Appを簡単につくれるフレームワーク。Websocketを使うmodeもありますが、今回はhttpを使うmodeを使用しています。 Flask PythonのWebフレームワークです。Flask上でSlack Boltを起動しています。 Vertex AI LLMの実行環境です。今回はファンデーションモデルにGemini 1.5 Flashを選んでいます。 SQLite ファイル保存形式の軽量なDBMS。SlackのUser TokenなどのUser情報を保存するために利用しています。 Litestream SQLiteをGCSなどにロジカルレプリケーションできるツール。Cloud Runがゼロスケールした際にSQLiteのDBファイルが破棄されてデータが消えてしまう問題に対処するために利用しています。コンテナがコールドスタートする際にGCSからDBファイルを復元しています。 Cloud RunにGCSをボリュームマウントし、そこにDBファイルを置いてもよかったのですが、レスポンスの速さを考えてDBファイルはコンテナに持たせるようにしました。 BigQuery 分析基盤です。プロンプト・編集前後のメッセージ・ユーザー自身が手動で変更等行い最終確定したメッセージの4つを履歴として保存しておき、Gen AI evaluation service等を利用してプロンプトやファンデーションモデルの評価・改善に利用します。 実装手順 slack appのインストールとtokenの取得 こちら のドキュメントを参考にslack appのインストールとtokenを取得してください。 TokenのScopeは以下のキャプチャのように付与してください。 App home -> App Display Nameで表示名をSaveしないとworkspaceへのAppのインストール時に以下のようなエラーが出るのでお気をつけください。 tokenをSecret Managerに登録 SLACK_BOT_TOKEN と SLACK_SIGNING_SECRET をCloud Runから読み込めるようにSecret Managerへ登録しておきます。 アプリケーションコードの説明 ディレクトリ構成 ├── Dockerfile ├── Makefile ├── main.py ├── requirements.txt ├── slack_util_tools.db main.py import os import logging from slack_bolt import App from slack_bolt.adapter.flask import SlackRequestHandler from flask import Flask, request import vertexai from vertexai.generative_models import GenerativeModel import sqlite3 logger = logging.getLogger(__name__) app = App( token=os.environ.get( "SLACK_BOT_TOKEN" ), signing_secret=os.environ.get( "SLACK_SIGNING_SECRET" ) ) flask_app = Flask(__name__) handler = SlackRequestHandler(app) vertexai.init(project=os.environ.get( "PROJECT_ID" ), location=os.environ.get( "LOCATION" )) model = GenerativeModel( "gemini-1.5-flash-002" ) @ app.event ( "reaction_added" ) def reaction_added (say, event): emoji = event[ "reaction" ] user = event[ "user" ] # 自分のmessageに対してスタンプを押したとき if emoji == "メッセージ編集" and "item_user" in event and user == event[ "item_user" ]: channel = event[ "item" ][ "channel" ] ts = event[ "item" ][ "ts" ] thread_ts = None message_text = None #スタンプを押したmessageの取得 conversations_history = app.client.conversations_history( channel=channel, oldest=ts, latest=ts, inclusive= True ,limit= 1 ) if not conversations_history[ "messages" ]: #スレッド内のmessageへのスタンプだったとき reply_history = app.client.conversations_replies( channel=channel, ts=ts) message_text = reply_history[ "messages" ][ 0 ][ "text" ] thread_ts = reply_history[ "messages" ][ 0 ][ "thread_ts" ] else : #通常のメッセージへのスタンプだったとき message_text = conversations_history[ "messages" ][ 0 ][ "text" ] response = model.generate_content( f """ 以下のメッセージを丁寧にしてください。 候補を出すのではなく最適な1つのメッセージのみを答えてください。 もし相手を傷つけてしまいそうな感情的な文章の場合は、相手を思いやった文章に編集してください。 {message_text} """ ) #DBファイルからuser_oauth_tokenの取得 conn = sqlite3.connect(os.environ.get( "DB_NAME" )) cur = conn.cursor() res = cur.execute( f "SELECT user_token FROM user WHERE user_id = '{user}'" ) user_token = res.fetchone()[ 0 ] conn.close() result = app.client.chat_postMessage( channel=event[ 'item' ][ 'channel' ], thread_ts=thread_ts, token=user_token, text=response.text, ) logger.info(result) @ app.event ( "reaction_removed" ) def reaction_removed (say, event): emoji = event[ "reaction" ] user = event[ "user" ] if emoji == "メッセージ編集" and "item_user" in event and user == event[ "item_user" ]: # DBファイルからuser_oauth_tokenの取得 conn = sqlite3.connect(os.environ.get( "DB_NAME" )) cur = conn.cursor() res = cur.execute( f "SELECT user_token FROM user WHERE user_id = '{user}'" ) user_token = res.fetchone()[ 0 ] conn.close() result = app.client.chat_delete( channel=event[ 'item' ][ 'channel' ], token=user_token, ts=event[ "item" ][ "ts" ], ) logger.info(result) @ flask_app.route ( "/slack/events" , methods=[ "POST" ]) def slack_events (): payload = request.get_json() if 'challenge' in payload: #チャレンジリクエストのとき return payload[ 'challenge' ] else : return handler.handle(request) @ app.middleware def skip_retry (logger, request, next ): if "x-slack-retry-num" not in request.headers: #再送リクエストでないとき return next () # ローカル開発用 if __name__ == "__main__" : flask_app.run(debug= True , host= "0.0.0.0" , port= int (os.environ.get( "PORT" , 3333 ))) 補足が必要そうな部分を説明します。 slack上のメッセージはchannelとtsで特定されます。 メッセージの取得にはconversations.history API、スレッド内のメッセージの取得にはconversations.replies APIを利用する必要があるため、以下のようにhistory APIで取得出来なかった場合にreplies APIに切り替えています。 #スタンプを押したmessageの取得 conversations_history = app.client.conversations_history( channel=channel, oldest=ts, latest=ts, inclusive= True ,limit= 1 ) if not conversations_history[ "messages" ]: #スレッド内のmessageへのスタンプだったとき reply_history = app.client.conversations_replies( channel=channel, ts=ts) 以下のコードのチャレンジリクエストの場合の処理がないと、後ほど説明するslack appへのEvent Subscriptionsの設定時に認証エラーとなってしまいます。 @ flask_app.route ( "/slack/events" , methods=[ "POST" ]) def slack_events (): payload = request.get_json() if 'challenge' in payload: #チャレンジリクエストのとき return payload[ 'challenge' ] else : return handler.handle(request) Slack APIには「3秒以内に応答がないとリトライされる 」という制約があります。 その制約に対処するために以下のようにリクエストヘッダーをみてリトライの場合は処理をしないようにしています。 @ app.middleware def skip_retry (logger, request, next ): if "x-slack-retry-num" not in request.headers: #再送リクエストでないとき return next () Cloud Runへのデプロイ 今回は手動でデプロイします。以下のようなDockerfileとrequirements.txtを用意します。 Dockerfile FROM python:3.12-bookworm ENV PYTHONUNBUFFERED True ENV APP_HOME /app WORKDIR $APP_HOME COPY . ./ RUN pip install -U pip && pip install -r requirements.txt ENTRYPOINT gunicorn --bind :$PORT --workers 1 --threads 2 --timeout 0 main:flask_app requirements.txt flask google-cloud-aiplatform gunicorn slack-bolt デプロイを実行します。今回はmakeファイルを用意しているので make deploy とコマンドを打てばOKです。 Makefile # 環境変数 PROJECT_ID={プロジェクトID} SERVICE_NAME={サービス名} LOCATION={リージョン} DB_NAME={DBファイル名} IMAGE_NAME=gcr.io/$(PROJECT_ID)/$(SERVICE_NAME) SERVICE_ACCOUNT=${SERVICE_NAME}@$(PROJECT_ID).iam.gserviceaccount.com # シークレット情報 SECRETS=SLACK_BOT_TOKEN={bot tokenを保存しているシークレット名}:latest,SLACK_SIGNING_SECRET={signing secretを保存しているシークレット名}:latest deploy: gcloud builds submit --tag $(IMAGE_NAME) gcloud run deploy $(SERVICE_NAME) --image $(IMAGE_NAME) \ --platform managed \ --service-account $(SERVICE_ACCOUNT) \ --region $(LOCATION) \ --update-secrets=$(SECRETS) \ --set-env-vars "PROJECT_ID=${PROJECT_ID}" \ --set-env-vars "LOCATION=${LOCATION}" \ --set-env-vars "DB_NAME=${DB_NAME}" \ gcloud run deploy コマンドの --update-secrets オプションにシークレットマネージャに保存しているtokenのパスを指定すると、デプロイ時にシークレットの値を環境変数として設定することができます。 Event Subscriptionsの設定 Slack AppのEvent Subscriptionsを有効化します。 Request URLにはCloud RunのURLに /slack/events というディレクトリ名を付与したものを設定してください。 Subscribe to bot eventsには reaction_added と reaction_removed を設定してください。 Slack Channelへインテグレーションの追加 任意のSlack Channelへ作成したSlack Appを追加したら完了です。 追加方法はいくつかあるのですが、追加したいChannelでSlack Appに対してメンションを投げることで追加する方法がお手軽かと思います。 終わりに メッセージの編集を行うプロンプトに以下のような命令を入れていました。 もし相手を傷つけてしまいそうな感情的な文章の場合は、相手を思いやった文章に編集してください。 これは“空気の読めるAI“のようなものが人間同士のコミュニケーションの間に入ってきて、受け手にとって最適な解釈ができるように“いい感じ“にしてくれるのを期待して入れています。今回はテキストコミュニケーションですが、音声コミュニケーションについてもあと何回かブレイクスルーが起きて、同様な事が出来るようになったら面白いのではないかと感じています。 著者自身はAI開発は素人ではありますが、社内の詳しい方に話を聞くたびに、そんな未来がくるかも、とワクワクしてしまいます!(妄言多謝) 最後に弊社採用もオープンしておりますので、気になりましたらお気軽にご応募くださいね!お話できることを楽しみにしています。 hrmos.co 参考 Getting started over HTTP | Bolt for Python bolt-python/examples/google_cloud_run/flask-gunicorn at main · slackapi/bolt-python · GitHub 【Slack】インストールするボットユーザーがありませんと出たときの対処方法 | THE SIMPLE Slack botをCloud Runで動かしてみた|まりーな/エンジニア Slack BoltをGoogle Cloudにデプロイするノウハウ conversations.history method | Slack conversations.replies method | Slack
アバター
はじめに Full-stack チームの豊崎です。 RevComm では、MiiTel Analytics の議事録作成をはじめ、LLM を用いた機能開発が活発に行われています。 今回、社内ユーザーが誰でも利用できる RAG 環境を作成しました。これは、RAG 環境をユーザーに提供するための PoC として実施したものです。 構成 この PoC では、プロダクトへの直接的な機能の埋め込みは行わず、以下のような構成で実装しました。 Amazon Bedrock, Amazon Bedrock Knowledge Bases ベクターストア: Pinecone データソース: S3 dynamoDB Python langchain streamlit etc… 問題点 この PoC を開始した時点での主な課題は、次の疑問から生まれました。 "Amazon Bedrock Knowledge Basesを使って、どのようにユーザーごとにRAG環境を提供できるのか?" 各ユーザーに個別の Amazon Bedrock Knowledge Bases を作成することは現実的ではなく、この課題に苦心しました。 結論として、ベクトルストアからデータを取得する際のフィルタリング機能が不可欠だと判明しました。 メタデータフィルタリング Amazon Bedrock Knowledge Bases には メタデータフィルタリング という機能があります。 これこそが、私の課題を解決する機能でした。 使い方 Knowledge Bases のデータソースに配置される各文書に対して、カスタムメタデータファイル( .metadata.json )を作成する必要があります。 このメタデータを利用して、ベクトルデータのフィルタリングを行います。 今回の目的はユーザーごとの RAG 環境提供ですが、このフィルタリングにより検索対象のチャンク数を削減でき、パフォーマンスと正確性の向上も実現できます。 以下が metadata.json のフォーマットです。 例 ) miitel_analytics_dashboard_overview . pdf をデータソースに配置した場合 // miitel_analytics_dashboard_overview.pdf.metadata.json { "metadataAttributes" : { "session_id" : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" , "user_id" : "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" , } } metadataAttributes 配下には、フィルタリング時に利用したい key-value を設定できます。 私は、ユーザーがファイルをアップロードする際に、このメタデータファイルが自動生成されるように実装しました。 サンプル (Python) 今回、私は langchain_community.retrievers.bedrock で提供されている AmazonKnowledgeBasesRetriever を利用しました。以下にそのサンプルコードを掲載します。 先ほどの metadata.json で設定した session_id と user_id をここで指定しています。 retriever = AmazonKnowledgeBasesRetriever ( knowledge_base_id = BEDROCK_KNOWLEDGE_BASE_ID , retrieval_config = { "vectorSearchConfiguration" : { "numberOfResults" : 10 , "filter" : { "andAll" : [ { "equals" : { "key" : "session_id" , "value" : "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX" , } , } , { "equals" : { "key" : "user_id" , "value" : "YYYYYYYY-YYYY-YYYY-YYYY-YYYYYYYYYYYY" , } , } , ] } , } , } , ) まとめ 今回は、Amazon Bedrock Knowledge Bases のメタデータフィルタリング機能について紹介しました。この機能により、ユーザーごとに独自の RAG 環境を提供しながら、不要なベクトルデータを効率的に除外することが可能となりました。 当初目標とした「社内の人が誰でも使える RAG 環境」は実現できましたが、以下のような課題が残されています: データソースとの連携間隔 本番環境での実装の実現性 etc... 今後は、プロダクトへの RAG 環境の実装検討をさらに進め、MiiTel ユーザーの業務効率化により一層貢献できるよう取り組んでいきます。 ご清覧ありがとうございました。
アバター
はじめに こんにちは! RevComm のフロントエンドエンジニアの楽桑です。 私たちのコールセンターシステムでは、 GraphQL を使用してデータを管理しており、これまでは Recoil を使ってローカルステートを管理していました。 最近では、 Recoil の代わりに Apollo Client の Local Cache を採用し、サーバーデータの取得・管理をより簡潔かつ効率的に行っています。 この記事では、 Apollo Client のキャッシュ利用について紹介します。 背景 今までは Recoil を使ってグローバルな状態管理を行ってきました。Recoilには以下のようなメリットがあります: 使いやすさ :シンプルで直感的に状態管理が可能。 学習コストが低い :初学者でも簡単に扱える設計。 プロジェクト初期の段階においては、これらのメリットを活かして素早く状態管理を整えることができ、 Recoil は悪くない選択肢でした。 しかし、サーバーから取得したデータを管理するケースにおいては、以下のような課題が浮き彫りになりました: データ同期の手動実装が必要 サーバーから取得したデータをローカルの Recoil 状態と同期させるには、追加の実装が必要です。これにより、コードの冗長化や保守性の低下を招きます。 データ取得の効率化が困難 同じデータを複数のコンポーネントで使用する場合、無駄なAPIリクエストが発生しやすく、パフォーマンスが低下します。 最新データの取得とパフォーマンスのトレードオフ リアルタイム性が求められる場合、常にサーバーからデータを取得する実装ではパフォーマンスの劣化を避けられません。 こうした背景から、サーバーと連携した効率的な状態管理を実現するために、 Apollo Client の導入を決めました。 Apollo Client は、 GraphQL の強力なキャッシュ管理を活用し、データ取得の効率化と同期の手間を軽減することで、これらの課題を解決します。 Apollo Client Cache とは Apollo Client Cache は、GraphQLを使ったデータ取得の効率を最大化するためのキャッシュ機能です。 一度取得したデータをクライアント側に保存し、再利用することでネットワークリクエストの削減など多くメリットある強力な機能です。 実装例 これまでのRecoilを用いた実装では、まずRecoil Atomを定義するところから始める必要がありました。 export const userState = atom ({ key : 'userState' , default : [] , }) ; たとえば useGetUser などのフックを定義する場合、データ取得が成功したタイミングで onCompleted コールバック内から手動で Recoil State へデータをセットする必要があります。 const useFetchUsersWithRecoil = () => { const [ users , setUsers ] = useRecoilState ( userState ) ; const [ getUsers , { data , loading , error }] = useLazyQuery ( GET_USERS , { fetchPolicy : 'network-only' , onCompleted : ( data ) => { setUsers ( data . users ) ; } }) ; return { getUsers , users , loading , error } ; } ; また、 Recoil State を更新するたびに再レンダリングが発生するため、 useLazyQuery を用いて取得回数を必要最低限に抑える必要があるなど、いくつかのデメリットも存在します 一方、 Apollo Client のローカルキャッシュ機能( Local Cache )のみを用いる場合、 Recoil State の定義や初期設定といった手順は不要になります。 const useFetchUsersWithApollo = () => { const { data , loading , error } = useQuery ( GET_USERS , { fetchPolicy : 'cache-first' , }) ; const users = data ?. users ?? [] ; return { users , loading , error } ; } ; fetchPolicy を cache-first に設定すると、 Apollo Client はすでにローカルキャッシュ上に存在するデータを優先的に返し、サーバーへの新規リクエストを行わなくなります。 これは、同じデータを何度も取得する必要がない場面でのパフォーマンス最適化につながり、不要なネットワーク通信を削減することが可能になります。 サブスクリプション動作 WebSocket を使用してデータの更新をサブスクリプションで行う場合の例をご紹介します。 従来の Recoil State を使ったデータ更新では、手動で setState を呼び出す必要がありました。以下はその実装例です: export const useUserSubscription = () => { const [ users , setUsers ] = useRecoilState ( usersState ) ; // Users配列の状態を取得・更新 useSubscription ( USER_UPDATED_SUBSCRIPTION , { onSubscriptionData : ({ subscriptionData }) => { if ( subscriptionData . data ?. userUpdated ) { const updatedUser = subscriptionData . data . userUpdated ; // 現在のusersを直接参照して更新 const updatedUsers = users . map (( user ) => user . id === updatedUser . id ? { ... user , ... updatedUser } : user ) ; setUsers ( updatedUsers ) ; } } , }) ; } ; 一方、 Apollo Client が提供する Local Cache を利用する場合、コードは非常に簡潔になります。 以下はその例です: export const useUserSubscription = () => { useSubscription ( USER_UPDATED_SUBSCRIPTION ) ; } ; そして、特定のユーザー情報を更新する場合、 Cache に keyFields を追加することで、 Apollo Client はそのユーザーのキャッシュのみを自動的に更新することが可能です。 export const graphqlCache = new InMemoryCache ({ typePolicies : { User : { keyFields : [ account_id ] } } }) このように、サブスクリプションデータの更新時に特定の副作用( Side Effect )が必要ない場合、非常にシンプルな実装が可能です。 カスタムキャッシュマージ Apollo Client では、フェッチしたデータがキャッシュ上に既に存在する場合、そのデータをどのように更新・統合(マージ)するかを柔軟に制御することができます。 これを typePolicies や merge 関数を用いて実現できます。 今回は、ユーザー情報のマスキングを例として挙げます。 たとえば、ユーザー情報を管理者のみが閲覧できる仕様にしたい場合、権限判定に応じたマスキング処理をフロントエンド側で行うケースを考えてみます。 まず、ログイン中のユーザー情報を取得し、管理者かどうかを判定します。 管理者であれば、すべてのユーザー情報を開示しますが、管理者でない場合は、 maskUser 関数を使用して隠したい情報をマスキングするように実装します。 export const graphqlCache = new InMemoryCache ({ typePolicies : { User : { keyFields : [ account_id ] merge ( existing , incoming , { cache } ) { const myAccount = cache . readQuery<MyAccountQuery> ({ query : GET_MY_ACCOUNT , }) ?. myAccount ; if ( ! myAccount ) { return incoming ; } const { account_id , permissions } = myAccount ; const isAdmin = permissions ?. includes ( 'is_admin' ) || false ; if ( isAdmin ) { return incoming ; } const maskedUser = maskingUser ( incoming , account_id ) ; return { ... existing , ... maskedUser , } ; } , } , } , }) ; このように、サーバーからユーザーのデータが更新される際に、キャッシュを更新する動作をカスタマイズすることができます。 終わり 最後までお読みいただきありがとうございます! この記事では、 Apollo Client を活用した GraphQL キャッシュの活用方法について解説しました。 Recoil から Apollo Client への移行を通じて、サーバーデータの効率的な取得やキャッシュ管理がどれほど便利かをご理解いただけたと思います。 Apollo Client の強力なキャッシュ機能は、単なるデータ取得の効率化だけでなく、アプリケーション全体のパフォーマンス向上やコードの保守性の向上にも寄与します。 特に、カスタムキャッシュマージを活用することで、フロントエンドでの柔軟なデータ操作が可能になります。 プロジェクトの要件によって最適な状態管理ツールは異なりますが、サーバーデータとの連携が必要な場合、 Apollo Client は非常に強力な選択肢です。 この記事が、皆さんのアプリケーション開発の参考になれば幸いです。
アバター