- Vue.js
Vue composableの作り方|共通化の例とmixinとの違い
Vue の composable(コンポーザブル)とは、ref や computed、ライフサイクルフックなどを使った「状態を持つロジック」を、use で始まる関数にまとめて再利用するしくみです。 複数のコンポーネントで同じ処理を書いていることに気づいたら、関数に切り出して共通化できます。
この記事では、そのままコピーして使える composable を TypeScript の型つきで6つ載せ、すべて Vue 3.5.43 で動かした結果を添えています。mixin との違いは比較表と対比コードで整理し、「分割代入したら画面が更新されなくなった」「setup の外で呼んだら警告が出た」といったつまずきやすい場面も、実際の出力と直し方をセットで確認できます。
composableの早見表
| composable | 用途 | 引数 | 戻り値 |
|---|---|---|---|
useCounter |
数を増減する | 初期値、{ min, max } |
{ count, double, inc, dec, reset } |
useToggle |
真偽値を切り替える | 初期値 | [state, toggle] |
useFetch |
URL からデータを取得する | URL(文字列・ref・getter) | { data, error, loading } |
useLocalStorage |
localStorage と同期する | キー、初期値 | ref |
useDebounce |
入力が落ち着いてから値を反映する | 値(ref・getter)、待ち時間 | ref |
useEventListener |
イベントを登録し、自動で解除する | 対象、イベント名、ハンドラ | 解除用の関数 |
作るときの決まりは次の5つです。
| 決まり | 理由 |
|---|---|
名前は use で始める |
composable だとひと目でわかる |
script setup または setup() の中で、同期的に呼ぶ |
ライフサイクルフックや後片付けを、呼んだコンポーネントに結びつけるため |
| ref をまとめたふつうのオブジェクトで返す | 分割代入してもリアクティブなまま使える |
引数は ref・getter・値のどれでも受け、toValue() で取り出す |
呼ぶ側の書き方を選ばず、変更にも追従できる |
タイマーやイベントは onScopeDispose などで後片付けする |
コンポーネントが消えたあとに処理が残らない |
確認した環境
| 項目 | バージョン |
|---|---|
| vue | 3.5.43 |
| @vue/test-utils / jsdom | 2.5.1 / 30.1.1 |
| TypeScript | 7.0.2 |
| Vite / @vitejs/plugin-vue | 8.3.2 / 6.0.9 |
| Node.js | 22.22.2 |
composable の動きは、Node.js 上で jsdom と @vue/test-utils を使い、template を文字列で書いたコンポーネントをマウントして確かめました。単一ファイルコンポーネント(SFC)で使う例は、Vite でビルドして Chromium(Playwright 1.56.0)で動かしています。型エラーの文は tsc --noEmit の実際の出力です。SFC の型チェック(vue-tsc 3.3.12)は、TypeScript 7.0.2 との組み合わせでは起動できなかったため未実行です。
composableの作り方の基本
いちばん小さな例として、カウンターを composable にします。コンポーネントの script setup に書いていた ref・computed・関数を、そのまま関数の中に移して return するだけです。
src/composables/useCounter.ts
import { computed, ref, type ComputedRef, type Ref } from "vue";
export interface UseCounterOptions {
min?: number;
max?: number;
}
export interface UseCounterReturn {
count: Ref<number>;
double: ComputedRef<number>;
inc: (delta?: number) => void;
dec: (delta?: number) => void;
reset: () => void;
}
export function useCounter(
initial = 0,
options: UseCounterOptions = {},
): UseCounterReturn {
const { min = -Infinity, max = Infinity } = options;
const count = ref(initial);
const double = computed(() => count.value * 2);
const clamp = (n: number) => Math.min(max, Math.max(min, n));
const inc = (delta = 1) => {
count.value = clamp(count.value + delta);
};
const dec = (delta = 1) => {
count.value = clamp(count.value - delta);
};
const reset = () => {
count.value = initial;
};
// ref をまとめた「ふつうのオブジェクト」で返す(分割代入しても ref のまま)
return { count, double, inc, dec, reset };
}
SFC から使う例です。
<script setup lang="ts">
import { useCounter } from "../../src/composables/useCounter";
import { useLocalStorage } from "../../src/composables/useLocalStorage";
// script setup の中で呼ぶだけで、テンプレートからそのまま使える
const { count, double, inc, reset } = useCounter(0, { max: 3 });
const memo = useLocalStorage("memo", "");
</script>
<template>
<button id="inc" @click="inc()">{{ count }} / {{ double }}</button>
<button id="reset" @click="reset()">リセット</button>
<input id="memo" v-model="memo" />
</template>
Vite でビルドして Chromium で操作した結果です(ボタンを4回押し、リセットし、メモを入力してリロード)。
SFC: 0 / 0 -> 1 / 2 -> 2 / 4 -> 3 / 6 -> 3 / 6 -> 0 / 0 | リロード後のメモ = 発注メモ | localStorage = "発注メモ"
max を 3 にしているので、4回目のクリックでは 3 のままです。戻り値の型(UseCounterReturn)を interface で書いておくと、使う側でエディタの補完が効き、書き間違いにもすぐ気づけます。
コピペで使えるcomposable集(TypeScriptの型つき)
useToggle:真偽値を切り替える
import { ref, type Ref } from "vue";
export function useToggle(
initial = false,
): [Ref<boolean>, (next?: boolean) => void] {
const state = ref(initial);
const toggle = (next?: boolean) => {
state.value = typeof next === "boolean" ? next : !state.value;
};
return [state, toggle];
}
配列で返すと、使う側で const [open, toggleOpen] = useToggle() のように好きな名前を付けられます。戻り値が2つ程度のときに向いた形です。
useToggle: 閉 -> 開 -> 閉
useFetch:URLが変わったら取り直す
import {
ref,
shallowRef,
toValue,
watchEffect,
type MaybeRefOrGetter,
} from "vue";
export function useFetch<T>(url: MaybeRefOrGetter<string>) {
const data = shallowRef<T | null>(null);
const error = shallowRef<Error | null>(null);
const loading = ref(false);
watchEffect((onCleanup) => {
const controller = new AbortController();
// URL が変わったら、前の通信を取り消す
onCleanup(() => controller.abort());
const target = toValue(url); // ref・getter・文字列のどれでも値を取り出せる
data.value = null;
error.value = null;
loading.value = true;
fetch(target, { signal: controller.signal })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json() as Promise<T>;
})
.then((json) => {
data.value = json;
})
.catch((e: unknown) => {
if (controller.signal.aborted) return;
error.value = e instanceof Error ? e : new Error(String(e));
})
.finally(() => {
if (!controller.signal.aborted) loading.value = false;
});
});
return { data, error, loading };
}
引数の型 MaybeRefOrGetter は「文字列そのもの・ref・関数(getter)のどれでもよい」という意味で、toValue() がその中身を取り出します。watchEffect の中で toValue(url) を読んでいるので、ref や getter を渡した場合は URL が変わるたびに自動で取り直します。
props の id から URL を組み立てる使い方です。
const props = defineProps<{ id: number }>();
const { data, error, loading } = useFetch<Item>(() => `/api/items/${props.id}`);
確認用のサーバーを立てて、id を 1 → 2 → 99(存在しない)と変えた結果です。
useFetch: 読み込み中 -> ノートPC -> 読み込み中 -> モニター -> エラー: HTTP 404
data の型は、型引数で渡した型と null の合併になります。null の可能性を確かめずに使うと、型チェックで知らせてくれます。
error TS18047: 'data.value' is possibly 'null'.
useLocalStorage:値をブラウザに保存する
import { ref, watch, type Ref } from "vue";
export function useLocalStorage<T>(key: string, initial: T): Ref<T> {
const read = (): T => {
try {
const raw = localStorage.getItem(key);
return raw === null ? initial : (JSON.parse(raw) as T);
} catch {
return initial; // 壊れた値が入っていたら初期値に戻す
}
};
const state = ref(read()) as Ref<T>;
watch(
state,
(value) => {
localStorage.setItem(key, JSON.stringify(value));
},
{ deep: true }, // オブジェクトの中身の変更も保存する
);
return state;
}
useLocalStorage: 初期表示 = light / 保存値 = null
useLocalStorage: クリック後 = dark / 保存値 = {"theme":"dark","size":14}
useLocalStorage: 作り直した後 = dark
useLocalStorage: 壊れた値のとき = light
初期値を渡すと型が推論されるので、useLocalStorage("settings", { theme: "light", size: 14 }) の戻り値は theme と size を持つオブジェクトの ref になります。deep: true を付けているので、settings.value.theme = "dark" のような中身の書き換えも保存されます。 サーバー側で描画する構成(SSR)では localStorage が無いので、その場合は onMounted の中で読む形に変えます(SSR での動作は未実行です)。
useDebounce:入力が落ち着いてから反映する
import {
onScopeDispose,
ref,
toValue,
watch,
type MaybeRefOrGetter,
type Ref,
} from "vue";
export function useDebounce<T>(
source: MaybeRefOrGetter<T>,
delay = 300,
): Readonly<Ref<T>> {
const debounced = ref(toValue(source)) as Ref<T>;
let timer: ReturnType<typeof setTimeout> | undefined;
watch(
() => toValue(source),
(value) => {
clearTimeout(timer);
timer = setTimeout(() => {
debounced.value = value;
}, delay);
},
);
// コンポーネントが消えるときにタイマーも止める
onScopeDispose(() => clearTimeout(timer));
return debounced;
}
検索欄の入力を 200ms 待ってから反映する例です。
const keyword = ref("");
const debounced = useDebounce(keyword, 200);
// debounced を useFetch の URL に使うと、入力のたびに通信せずに済む
useDebounce:
8ms 入力="モ" debounced=""
91ms 入力="モニ" debounced=""
173ms 入力="モニタ" debounced=""
354ms 入力="モニタ" debounced=""
455ms 入力="モニタ" debounced="モニタ"
80ms おきに3回入力し、最後の入力(173ms)から 200ms たったあとに、最後の値だけが反映されています。
useEventListener:登録と解除をセットにする
import { onScopeDispose, toValue, watch, type MaybeRefOrGetter } from "vue";
export function useEventListener<K extends keyof WindowEventMap>(
target: MaybeRefOrGetter<EventTarget | null | undefined>,
event: K,
handler: (event: WindowEventMap[K]) => void,
options?: AddEventListenerOptions,
): () => void {
const listener = handler as EventListener;
// target が ref(テンプレート参照など)のときは、要素が入った時点で登録する
const stopWatch = watch(
() => toValue(target),
(el, _prev, onCleanup) => {
if (!el) return;
el.addEventListener(event, listener, options);
onCleanup(() => el.removeEventListener(event, listener, options));
},
{ immediate: true },
);
// スコープ(コンポーネント)が終わったら自動で解除する
onScopeDispose(stopWatch);
return stopWatch;
}
const key = ref("");
useEventListener(window, "keydown", (e) => {
key.value = e.key; // e は KeyboardEvent と推論される
});
useEventListener: 表示 = a / 呼ばれた回数 = 1
useEventListener: アンマウント後に発火 → 呼ばれた回数 = 1
コンポーネントをアンマウントしたあとは、イベントが起きてもハンドラが呼ばれていません。 addEventListener と removeEventListener を毎回ペアで書く手間がなくなり、解除し忘れも防げます。
なお、これらに近い機能は VueUse というライブラリにもそろっています(この記事では VueUse は未実行です)。自分で書くと、しくみを理解したうえで必要な分だけ持てるのが利点です。
mixinとの違いを比較表と対比コードで見る
Vue 2 までは、ロジックの共通化に mixin がよく使われていました。mixin は Vue 3 でも使えますが、公式ガイドでは「プロパティの出どころが明確」「名前の衝突を避けられる」「依存が明示的」という3点で composable に利点があると説明されています。
| 比べる点 | mixin | composable |
|---|---|---|
| 名前の衝突 | 同じ名前があると、警告なしに一方で上書きされる | 受け取る側で名前を付け替えられる |
| 出どころの追いやすさ | テンプレートの count がどの mixin 由来か、mixin を開かないとわからない |
const { count } = useCounter() の1行でわかる |
| 型推論 | defineComponent で定義すれば効く。ただのオブジェクトだと this の型が付かない |
関数の引数と戻り値として、そのまま推論される |
| 引数の渡しやすさ | 渡せない(コンポーネント側の data などを暗黙に参照する) | 関数の引数として渡せる(初期値、オプション、ref) |
| 同じものを2回使う | できない(2つ目が1つ目と同じ名前になる) | できる(useCounter() を2回呼ぶ) |
| 使える場所 | Options API | Composition API(setup() の中なら Options API のコンポーネントでも可) |
同じ処理をmixinとcomposableで書く
mixin 版(src/mixin/counterMixin.ts)
import { defineComponent } from "vue";
// mixin:data・computed・methods をまとめて、コンポーネントに混ぜ込む
export const counterMixin = defineComponent({
data() {
return { count: 0 };
},
computed: {
double(): number {
return this.count * 2;
},
},
methods: {
inc() {
this.count++;
},
},
});
// 使う側(mixin)
export default defineComponent({
mixins: [counterMixin],
// テンプレートでは count / double / inc がそのまま使える
});
composable 版(先ほどの useCounter)
// 使う側(composable)
export default defineComponent({
setup() {
const { count, double, inc } = useCounter();
return { count, double, inc };
},
});
どちらも画面の動きは同じです。
mixin: 0 / 0 -> 1 / 2
composable: 0 / 0 -> 1 / 2
名前が衝突したときの違い
別の mixin が、たまたま同じ count と inc を使っていたとします。
import { defineComponent } from "vue";
// 別の人が作った mixin。たまたま同じ名前 count / inc を使っている
export const stockMixin = defineComponent({
data() {
return { count: 100 };
},
methods: {
inc() {
this.count += 10;
},
},
});
2つを同時に使った結果です(ボタンを1回クリック)。
衝突 mixins: [counterMixin, stockMixin]: 100 / 200 -> 110 / 220
衝突 mixins: [stockMixin, counterMixin]: 0 / 0 -> 1 / 2
警告の数: 0
配列の後ろに書いた mixin の count と inc が使われ、警告は出ませんでした。 mixin を足す順番で動きが変わるので、数が増えるほど追いにくくなります。
composable なら、受け取るときに名前を付け替えるだけで両方を使えます。
const { count: clicks, inc: addClick } = useCounter();
const { count: stock, inc: addStock } = useCounter(100);
composable 2 つ: clicks = 1 / stock = 110
型推論の違い
defineComponent を使わず、ただのオブジェクトで mixin を書くと、methods の中の this に data の型が付きません。
// defineComponent を使わない、ただのオブジェクトの mixin
export const plainMixin = {
data() {
return { count: 0 };
},
methods: {
inc() {
this.count++;
},
},
};
src/types/plainMixin.ts(8,12): error TS2339: Property 'count' does not exist on type '{ inc(): void; }'.
defineComponent で定義した mixin は型が効きますが、エラー文に出てくる型は長くなります。同じ書き間違い(double を doubel と書く)で比べました。
# mixin(this.doubel)
error TS2551: Property 'doubel' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<{}, {}>, {}, {}, {}, { report(): string; }, DefineComponent<{}, {}, { count: number; }, { double(): number; }, { inc(): void; }, ComponentOptionsMixin, ... 13 more ..., any>, ... 19 more ..., {}>'. Did you mean 'double'?
# composable(const { count, doubel } = useCounter())
error TS2339: Property 'doubel' does not exist on type 'UseCounterReturn'.
composable は「関数の戻り値」というふつうの TypeScript の型なので、エラー文も短く、どこを直せばよいかが読み取りやすくなります。
mixinからcomposableへ移すときの対応表
| mixin での書き方 | composable での書き方 |
|---|---|
data() { return { count: 0 } } |
const count = ref(0) |
computed: { double() {...} } |
const double = computed(() => ...) |
methods: { inc() {...} } |
const inc = () => {...} |
mounted() {...} |
onMounted(() => {...}) |
watch: { count(v) {...} } |
watch(count, (v) => {...}) |
this.count |
count.value |
コンポーネント側の値を this.xxx で参照 |
引数として受け取る |
リアクティブでなくなる例と直し方
composable を使い始めると、「値は変わっているはずなのに画面が更新されない」場面に出会うことがあります。原因はほぼ決まっていて、リアクティブなオブジェクトから「ただの値」を取り出してしまっていることです。
reactiveを分割代入するとただの値になる
// reactive なオブジェクトをそのまま返す composable
function useCounterReactive() {
const state = reactive({ count: 0 });
const inc = () => {
state.count++;
};
return { state, inc };
}
// 直す前:reactive を分割代入する
const { state, inc } = useCounterReactive();
const { count } = state; // ここでただの数値 0 になる
// 直した後:toRefs で ref に変えてから分割代入する
const { count } = toRefs(state);
ボタンを2回クリックした結果です。
直す前 isRef(count) = false / typeof = number
直す前(分割代入): 0 -> 0
直した後 isRef(count) = true
直した後(toRefs): 0 -> 2
reactive の分割代入は、その時点の値のコピーを取り出すだけなので、あとから state.count が増えても count は 0 のままです。toRefs() を通すと、各プロパティが元のオブジェクトとつながった ref になります。
| 返し方 | 分割代入したとき | おすすめ度 |
|---|---|---|
return { count, inc }(ref をまとめたオブジェクト) |
リアクティブなまま | 基本はこれ |
return toRefs(state) |
リアクティブなまま | reactive で書きたいとき |
return state(reactive そのもの) |
ただの値になる | 分割代入しない前提なら可 |
composable を作る側が「ref をまとめたオブジェクト」で返しておけば、使う側は気にせず分割代入できます。この記事の composable がすべてその形なのはこのためです。
propsの値をそのまま渡すと変更が伝わらない
もう1つ多いのが、props を composable に渡すときです。
function useLabel(id: MaybeRefOrGetter<number>) {
return computed(() => `備品 #${toValue(id)}`);
}
// 直す前:数値のコピーを渡している
const label = useLabel(props.id);
// 直した後:getter(関数)で渡す
const label = useLabel(() => props.id);
props の id を 1 から 2 に変えた結果です。
props.id をそのまま渡す: 備品 #1 -> 備品 #1
() => props.id で渡す: 備品 #1 -> 備品 #2
props.id と書いた時点で数値の 1 が取り出されるので、composable には「1」しか届きません。() => props.id という関数で渡すと、composable の中の toValue() が呼ぶたびに最新の値を読みに行きます。
toValueは3種類の入力を同じように扱う
const r = ref(3);
console.log("toValue:", toValue(1), toValue(r), toValue(() => r.value * 2));
toValue: 1 3 6
| 渡すもの | toValue() の結果 |
変更への追従 |
|---|---|---|
値そのもの(1) |
そのまま返る | しない |
ref(r) |
.value が返る |
する |
getter(() => r.value * 2) |
関数を呼んだ結果が返る | する |
composable の引数を MaybeRefOrGetter 型にして、computed や watchEffect の中で toValue() を呼ぶのがポイントです。外で一度だけ呼ぶと、そこで値が固定されてしまいます。
setupの外で呼ぶと出る警告と直し方
ライフサイクルフック(onMounted など)を使う composable は、script setup か setup() の実行中に、同期的に呼ぶ必要があります。Vue は「今どのコンポーネントの setup を実行しているか」を覚えていて、フックをそのコンポーネントに結びつけるためです。
確認に使った composable です。
function useMounted() {
const mounted = ref(false);
onMounted(() => {
mounted.value = true;
});
return mounted;
}
呼ぶ場所を変えて試した結果です。
| 呼んだ場所 | 結果 | 警告 |
|---|---|---|
| モジュールの先頭(setup の外) | mounted は false のまま | あり |
| クリックのハンドラの中 | false のまま | あり |
async setup() の中で、await のあと |
false のまま | あり |
async setup() の中で、await より前 |
true になる | なし |
警告文は3つとも同じでした。
[Vue warn]: onMounted is called when there is no active component instance to be associated with. Lifecycle injection APIs can only be used during execution of setup(). If you are using async setup(), make sure to register lifecycle hooks before the first await statement.
inject() を setup の外で呼んだ場合は、別の警告になり、値は undefined でした。
[Vue warn]: inject() can only be used inside setup() or functional components.
直し方は次のとおりです。
| 状況 | 直し方 |
|---|---|
| イベントハンドラや setTimeout の中で呼んでいる | composable は setup の先頭で呼び、ハンドラでは戻り値の関数や ref を使う |
async setup() で await のあとに呼んでいる |
await より前に呼ぶ |
| モジュールの先頭で呼んでいる | コンポーネントの setup の中へ移す |
// 直す前
const onClick = () => {
const { inc } = useCounter(); // クリックのたびに新しいカウンターができる
inc();
};
// 直した後
const { inc } = useCounter(); // setup の先頭で 1 回だけ呼ぶ
const onClick = () => inc();
なお、公式ドキュメントには「script setup は、await のあとでも composable を呼べる唯一の場所」と書かれています(コンパイラが実行中のコンポーネントを復元するため)。今回 await の前後で確かめたのは setup() 関数のほうで、script setup での await 後の呼び出しは未実行です。
状態を共有するか、コンポーネントごとに持つか
composable の中で作った ref は、呼ぶたびに新しく作られるので、コンポーネントごとに別々の状態になります。一方、ref を関数の外に置くと、すべての呼び出しで同じ状態を共有します。
// 関数の外:全コンポーネントで共有される
const shared = ref(0);
function useShared() {
return { shared, inc: () => shared.value++ };
}
// 関数の中:呼んだコンポーネントごとに別々
function useLocal() {
const local = ref(0);
return { local, inc: () => local.value++ };
}
同じコンポーネントを2つ(A と B)マウントし、A だけを2回クリックした結果です。
(6) A を 2 回クリック → A: 共有 2 / 個別 2 | B: 共有 2 / 個別 0
| 置き場所 | 状態 | 向いている用途 |
|---|---|---|
| 関数の中 | コンポーネントごとに独立 | フォームの入力、開閉の状態、個別のデータ取得 |
| 関数の外(モジュールの先頭) | アプリ全体で共有 | ログイン中のユーザー、テーマ、通知の一覧 |
共有する状態が増えてきたら、Pinia のような状態管理ライブラリに移すと、開発者ツールでの確認などがしやすくなります(Pinia は未実行です)。サーバー側で描画する構成では、モジュールの先頭に置いた状態がリクエストをまたいで共有される点に注意が必要です(一般に知られた仕様。未実行)。
よくある質問
Q. composable とふつうの関数(ユーティリティ)は何が違いますか? A. ref や computed、ライフサイクルフックなど Vue のリアクティブなしくみを中で使うのが composable です。日付の整形のように、入力から結果を返すだけの関数は、use を付けないふつうの関数として分けておくと、どこでも呼べて見分けもつきやすくなります。
Q. composable のファイルはどこに置きますか?
A. src/composables/ に、1ファイル1関数で useCounter.ts のように置くのが一般的です。Nuxt では composables/ ディレクトリに置くと自動で import されます(Nuxt での動作は未実行です)。
Q. Options API のコンポーネントからも使えますか?
A. 使えます。setup() の中で呼び、戻り値を return すると、テンプレートから参照できます。この記事の対比コード(composable 版)がその形です。
Q. composable の中で別の composable を呼んでもよいですか? A. はい。小さな composable を組み合わせて大きなものを作れます。たとえば useDebounce の戻り値を useFetch の URL に使うと、「入力が落ち着いてから取得する検索」になります。呼ぶ場所の決まり(setup の実行中に同期的に)は同じです。
まとめ
- composable は「状態を持つロジック」を use で始まる関数にまとめたもの。ref・computed・関数を関数の中へ移して return すれば作れる
- 戻り値は ref をまとめたふつうのオブジェクトにする。分割代入してもリアクティブなまま使える
- 引数は
MaybeRefOrGetterで受け、computed や watchEffect の中でtoValue()を呼ぶ。props は() => props.idの形で渡す - reactive を分割代入するとただの値になる。
toRefs()を通すと 0 のままだった表示が 2 に更新された - mixin は同じ名前があると警告なしに上書きされる。composable は受け取る側で名前を付け替えられ、型エラーも短く読みやすい
- composable は setup の実行中に同期的に呼ぶ。ハンドラの中や await のあとで呼ぶと「no active component instance」の警告が出る
- ref を関数の外に置くと共有、中に置くとコンポーネントごと。用途に合わせて選ぶ
参考
- Vue.js 公式ガイド「コンポーザブル」(https://ja.vuejs.org/guide/reusability/composables )
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。