- Vue.js
Vue defineModelの使い方|複数v-modelと修飾子をコードで解説
defineModel とは、子コンポーネントで親の v-model を受け取るためのマクロで、戻り値の ref を読み書きするだけで親の値と同期できる仕組みです。 これまで modelValue という props と update:modelValue というイベントを1組ずつ書いていた部分が、const model = defineModel() の1行にまとまります。公式ドキュメントでは、Vue 3.4 以降はこの書き方が推奨されています。
この記事では、従来記法と defineModel の対応表を最初に置き、複数の v-model・独自修飾子・get/set による変換・TypeScript の型指定を、すべてコピペで動く単一ファイルコンポーネント(SFC)で示します。あわせて、default を付けたときに「親は undefined のまま、子だけ値を持つ」状態になる挙動を Vitest で再現し、親子の値をそろえる方法を3通り紹介します。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Vue 3.5.43 / Vitest 5.0.3 / @vue/test-utils 2.5.1 / jsdom 30.1.1 / vue-tsc 3.3.12(TypeScript 6.0.3)
defineModelの早見表:従来記法との対応
| やりたいこと | 従来記法(props + emit) | defineModel |
|---|---|---|
| 値を受け取る | defineProps<{ modelValue: string }>() |
const model = defineModel<string>() |
| 値を読む | props.modelValue |
model.value(template では model) |
| 親へ変更を送る | emit('update:modelValue', v) |
model.value = v |
| input に結び付ける | :value と @input を両方書く |
v-model="model" |
| 必須にする | modelValue: string(? なし) |
defineModel<string>({ required: true }) |
| デフォルト値 | withDefaults など | defineModel({ default: 0 }) |
| 名前付き(複数) | props title + イベント update:title |
defineModel<string>('title') |
| 修飾子を受け取る | props modelModifiers |
const [model, modifiers] = defineModel() |
| 名前付きの修飾子 | props titleModifiers |
const [title, titleModifiers] = defineModel('title') |
| 値を変換する | emit の前後に自分で処理 | defineModel({ get, set }) |
| 親側の書き方 | v-model="x" / v-model:title="x" |
同じ(親は変更なし) |
親コンポーネントの書き方は、従来記法でも defineModel でも変わりません。 変わるのは子の中だけなので、子コンポーネントを1つずつ書き換えていけます。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue defineModelの使い方|複数v-modelと修飾子をコードで解説」
従来記法とdefineModelを並べて比べる
従来記法:modelValueとupdate:modelValue
<!-- src/LegacyInput.vue -->
<script setup lang="ts">
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', ($event.target as HTMLInputElement).value)"
/>
</template>
defineModelで書いた同じコンポーネント
<!-- src/ModelInput.vue -->
<script setup lang="ts">
const model = defineModel<string>({ required: true })
</script>
<template>
<input v-model="model" />
</template>
defineModel が返すのは ref です。 model.value を読めば親の値が取れ、model.value に代入すれば親へ update:modelValue が送られます。ref なので、ネイティブの input 要素に v-model="model" でそのまま渡せます。
modelValueとの違い:中では同じものが使われている
defineModel は、公式ドキュメントの説明どおり、modelValue という props と update:modelValue というイベントに展開されます。defineModel で書いた子(後述の DefaultCount.vue)に modelValue: 5 を渡してボタンを1回押し、発生したイベントを Vue Test Utils の emitted() で見た結果です。
emitted: [[6]]
update:modelValue イベントが、引数 6 で1回発生しています。つまり「modelValue と defineModel のどちらを使うか」は書き方の違いで、親から見た動きは同じです。
コピペで動くdefineModelの部品集
ここからの子コンポーネントは、すべて src フォルダに置いてそのまま動きます。最後に、全部をまとめて使う親コンポーネントと実行結果を載せます。
数値のv-model:カウンター
<!-- src/Counter.vue -->
<script setup lang="ts">
const count = defineModel<number>({ required: true })
</script>
<template>
<button @click="count++">+1(現在 {{ count }})</button>
</template>
template の中では .value なしで count++ と書けます。
複数のv-model:姓と名を別々に受け取る
defineModel の第1引数に名前を渡すと、名前付きの v-model になります。 1つのコンポーネントで何個でも宣言できます。
<!-- src/NameFields.vue -->
<script setup lang="ts">
const lastName = defineModel<string>('lastName', { required: true })
const firstName = defineModel<string>('firstName', { required: true })
</script>
<template>
<input class="last" v-model="lastName" placeholder="姓" />
<input class="first" v-model="firstName" placeholder="名" />
</template>
親からは v-model:last-name="last" のように、コロンのあとに名前を書いて渡します(ケバブケースで書いても、子の lastName に届きました)。
独自修飾子:先頭を大文字にする .capitalize
親が v-model.capitalize="x" と書いたとき、子はその修飾子を分割代入の2つ目で受け取れます。
<!-- src/CapInput.vue -->
<script setup lang="ts">
// 独自修飾子 .capitalize と .upper を受け取る
const [model, modifiers] = defineModel<string, 'capitalize' | 'upper'>({
required: true,
set(value) {
if (modifiers.upper) return value.toUpperCase()
if (modifiers.capitalize) return value.charAt(0).toUpperCase() + value.slice(1)
return value
},
})
</script>
<template>
<input v-model="model" />
</template>
set は「子から親へ送る直前」に呼ばれる関数で、戻り値が親に届きます。修飾子が付いているかどうかは modifiers.capitalize が true かどうかで判定します。型引数の2つ目('capitalize' | 'upper')に修飾子の名前を書いておくと、親が存在しない修飾子を書いたときに型チェックで気づけます(後述)。
名前付きv-modelの修飾子
名前付きの場合も同じ形で受け取れます。親は v-model:title.compact="title" と書きます。
<!-- src/TitleModifier.vue -->
<script setup lang="ts">
// 名前付き v-model の修飾子: v-model:title.compact
const [title, titleModifiers] = defineModel<string, 'compact'>('title', {
required: true,
set: (v) => (titleModifiers.compact ? v.replace(/\s+/g, '') : v),
})
</script>
<template>
<input v-model="title" />
</template>
get/setで値を変換する
get は「親の値を子が読むとき」、set は「子から親へ送るとき」の変換です。次の例は、親が小文字のコードを持ち、子の画面では大文字で見せます。
<!-- src/GetSet.vue -->
<script setup lang="ts">
// get: 親の値を子で読むときの変換 / set: 子から親へ送るときの変換
const code = defineModel<string>({
required: true,
get: (v) => v.toUpperCase(),
set: (v) => v.trim().toLowerCase(),
})
</script>
<template>
<label>
<input v-model="code" />
<span>{{ code }}</span>
</label>
</template>
表示と値の型が違う場合:カンマ区切りの金額入力
親は数値、入力欄はカンマ区切りの文字列、というように型が違う場合は、v-model を input に直接つながず、:value と @input に分けて書くとすっきりします。
<!-- src/PriceInput.vue -->
<script setup lang="ts">
// 親は数値(円)を持ち、入力欄にはカンマ区切りの文字列を見せる
const price = defineModel<number>({ required: true })
function onInput(e: Event) {
const digits = (e.target as HTMLInputElement).value.replace(/[^0-9]/g, '')
price.value = digits === '' ? 0 : Number(digits)
}
</script>
<template>
<input :value="price.toLocaleString('ja-JP')" @input="onInput" />
</template>
全部をまとめて使う親コンポーネント
<!-- src/ParentAll.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import LegacyInput from './LegacyInput.vue'
import ModelInput from './ModelInput.vue'
import Counter from './Counter.vue'
import NameFields from './NameFields.vue'
import CapInput from './CapInput.vue'
import PriceInput from './PriceInput.vue'
import GetSet from './GetSet.vue'
const a = ref('a')
const b = ref('b')
const n = ref(0)
const last = ref('山田')
const first = ref('太郎')
const cap = ref('')
const up = ref('')
const price = ref(1200)
const code = ref('ab-01')
</script>
<template>
<LegacyInput id="legacy" v-model="a" />
<ModelInput id="model" v-model="b" />
<Counter v-model="n" />
<NameFields v-model:last-name="last" v-model:first-name="first" />
<CapInput id="cap" v-model.capitalize="cap" />
<CapInput id="up" v-model.upper="up" />
<PriceInput id="price" v-model="price" />
<GetSet id="code" v-model="code" />
<pre>{{ JSON.stringify({ a, b, n, last, first, cap, up, price, code }) }}</pre>
</template>
実行結果
Vitest と jsdom でこの親をマウントし、各入力欄に値を入れた前後で、親が持つ値(pre 要素の中身)を出力しました。入力した値は順に「従来」「新記法」、ボタン1回クリック、「佐藤」「花子」、「hello」(2か所)、「98,000円」、前後に空白を付けた「 XY-99 」です。
初期: {"a":"a","b":"b","n":0,"last":"山田","first":"太郎","cap":"","up":"","price":1200,"code":"ab-01"}
操作後: {"a":"従来","b":"新記法","n":1,"last":"佐藤","first":"花子","cap":"Hello","up":"HELLO","price":98000,"code":"xy-99"}
price表示: 98,000 / code表示: XY-99
| 部品 | 入力 | 親に届いた値 | 何が起きたか |
|---|---|---|---|
| LegacyInput / ModelInput | 従来 / 新記法 | そのまま | 2つの記法で結果は同じ |
| NameFields | 佐藤・花子 | last と first に別々に入る | 複数 v-model |
| CapInput(.capitalize) | hello | Hello | set で先頭を大文字に |
| CapInput(.upper) | hello | HELLO | 同じ子で修飾子ごとに分岐 |
| PriceInput | 98,000円 | 98000(数値) | 数字だけ取り出して数値に |
| GetSet | 「 XY-99 」 | xy-99 | set で trim と小文字化。画面は get で XY-99 |
名前付き修飾子の TitleModifier も、親で v-model:title.compact="title" として「 週 次 報告 」(空白入り)を入力すると、親の値は「週次報告」になりました。
修飾子の一覧と従来記法の対応
組み込みの .trim と .number はコンポーネントでも効く
ModelInput(defineModel の型は string)に対して、親で v-model.trim と v-model.number を付けて試した結果です。
{"title":"週次報告","cap":"Hello","trim":"abc","number":42,"typeofNumber":"number"}
| 親の書き方 | 入力 | 親に届いた値 |
|---|---|---|
v-model.trim="t" |
「 abc 」 | "abc" |
v-model.number="n" |
42 | 42(number 型) |
子に何も書かなくても、.trim と .number は親に届く値に反映されました。 ただし .number を付けると親の値が数値になり、子が string として宣言している場合は次の警告が出ます。数値を扱う子は defineModel<number>() で宣言しておくと型がそろいます。
[Vue warn]: Invalid prop: type check failed for prop "modelValue". Expected String with value "42", got Number with value 42.
従来記法で修飾子を受け取る:modelModifiers
defineModel を使わない場合、修飾子は modelModifiers という props で届きます(名前付きなら「名前 + Modifiers」)。先ほどの .capitalize を従来記法で書くとこうなります。
<!-- src/LegacyCap.vue -->
<script setup lang="ts">
const props = defineProps<{
modelValue: string
modelModifiers?: { capitalize?: boolean }
}>()
const emit = defineEmits<{ 'update:modelValue': [value: string] }>()
function onInput(e: Event) {
let value = (e.target as HTMLInputElement).value
if (props.modelModifiers?.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input :value="props.modelValue" @input="onInput" />
</template>
親で v-model.capitalize="cap" として hello を入力すると、親の値は Hello になりました(上の出力の cap)。defineModel 版の CapInput と同じ結果で、defineModel 版のほうが props とイベントの宣言がない分だけ短く書けます。
TypeScriptの型指定
書き方ごとの型
vue-tsc に .value の型を表示させた結果です。
| 書き方 | .value の型 |
|---|---|
defineModel<string>() |
string | undefined |
defineModel<string>({ required: true }) |
string |
defineModel<string>({ default: '' }) |
string |
defineModel({ type: Number, default: 0 }) |
number |
defineModel('flag', { type: Boolean }) |
boolean | undefined |
defineModel<string, 'upper' | 'compact'>('f') の修飾子 |
Record<"upper" | "compact", true | undefined> |
型引数だけを書くと undefined が含まれます。 親が必ず値を渡す設計なら required: true を付けると、.value をそのまま使えるようになります。
親が違う型や存在しない修飾子を渡したとき
<script setup lang="ts">
import { ref } from 'vue'
import Counter from './Counter.vue'
import CapInput from './CapInput.vue'
const text = ref('1')
</script>
<template>
<Counter v-model="text" />
<CapInput v-model.uper="text" />
</template>
vue-tsc の表示:
src/TypeErr.vue(9,12): error TS2322: Type 'string' is not assignable to type 'number'.
src/TypeErr.vue(10,21): error TS2561: Object literal may only specify known properties, but 'uper' does not exist in type 'Partial<Record<"capitalize" | "upper", true>>'. Did you mean to write 'upper'?
1行目は、数値用の Counter に文字列の ref を渡したことへの指摘です。2行目は修飾子のつづり違いで、正しい候補(upper)まで提案してくれます。修飾子の名前を defineModel の2つ目の型引数に書いておく効果がここで出ます。
defaultを付けると親がundefinedのままになる挙動
Vitestで再現する
公式ドキュメントには、defineModel に default を指定し、親がその props に値を渡さなかった場合、親子で同期が取れなくなる可能性があるという注意書きがあります。これを実際に動かして確かめます。
<!-- src/DefaultCount.vue -->
<script setup lang="ts">
const count = defineModel<number>({ default: 1 })
</script>
<template>
<button @click="count++">子: {{ count }}</button>
</template>
<!-- src/ParentDefault.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import DefaultCount from './DefaultCount.vue'
const count = ref<number>() // 初期値なし = undefined
</script>
<template>
<p>親: {{ String(count) }}</p>
<DefaultCount v-model="count" />
</template>
テストコード(test/default.test.ts):
import { mount } from '@vue/test-utils'
import { test } from 'vitest'
import ParentDefault from '../src/ParentDefault.vue'
test('ParentDefault', async () => {
const w = mount(ParentDefault)
console.log('[ParentDefault] マウント直後 →', w.text())
await w.find('button').trigger('click')
console.log('[ParentDefault] 1回クリック →', w.text())
})
実行結果(npx vitest run --disableConsoleIntercept):
[ParentDefault] マウント直後 → 親: undefined子: 1
[ParentDefault] 1回クリック → 親: 2子: 2
マウント直後は、親が undefined、子が 1 と、値がそろっていません。 default は子の中だけで使われる値で、親へは送られないためです。子で値を変更した時点で初めて update:modelValue が送られ、親子がそろいます。この状態のままフォームを送信すると、画面には 1 と見えているのに、親が持つデータは undefined、ということが起こります。
そろえ方A:初期値を親が持ち、子はrequiredにする
いちばんシンプルなのは、初期値を親に一本化する方法です。
<!-- src/RequiredCount.vue -->
<script setup lang="ts">
const count = defineModel<number>({ required: true })
</script>
<template>
<button @click="count++">子: {{ count }}</button>
</template>
<!-- src/ParentFixA.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import RequiredCount from './RequiredCount.vue'
const count = ref(1) // 初期値は親が持つ
</script>
<template>
<p>親: {{ String(count) }}</p>
<RequiredCount v-model="count" />
</template>
[ParentFixA] マウント直後 → 親: 1子: 1
[ParentFixA] 1回クリック → 親: 2子: 2
required にしておくと、親が v-model を付け忘れたときに次の警告が出るので、渡し忘れにも気づけます。
[Vue warn]: Missing required prop: "modelValue"
そろえ方B:defaultを使わず、表示用の値だけ子で補う
親が「値がまだない」状態を持ちたい場合は、子の defineModel に default を付けず、表示に使う値だけ computed で補います。
<!-- src/FallbackCount.vue -->
<script setup lang="ts">
import { computed } from 'vue'
const count = defineModel<number>() // default を付けない
const shown = computed(() => count.value ?? 1) // 表示用の補完だけ子で行う
</script>
<template>
<button :class="{ unset: count === undefined }" @click="count = shown + 1">
子: {{ shown }}{{ count === undefined ? '(仮の値)' : '' }}
</button>
</template>
[ParentFixB] マウント直後 → 親: undefined子: 1(仮の値)
[ParentFixB] 1回クリック → 親: 2子: 2
親の値は undefined のままですが、子の count も undefined で一致しており、「まだ選ばれていない」ことを画面に出せます。 見た目の値と本当の値を分けて扱える書き方です。
そろえ方C:親の側で既定値を入れる
API の応答などで値が空のまま届くことがある場合は、親でマウント時に既定値を入れてそろえます。
<!-- src/ParentFixC.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import DefaultCount from './DefaultCount.vue'
const DEFAULT_COUNT = 1
const count = ref<number>()
// 外部から届いた値が空だったときに、親の側で既定値を入れてそろえる
onMounted(() => {
count.value ??= DEFAULT_COUNT
})
</script>
<template>
<p>親: {{ String(count) }}</p>
<DefaultCount v-model="count" />
</template>
[ParentFixC] マウント後 → 親: 1子: 1
[ParentFixC] 1回クリック → 親: 2子: 2
(こちらは onMounted のあと、nextTick を待ってから出力しています。)
3つの方法の選び方
| 状況 | 向いている方法 |
|---|---|
| 親が必ず初期値を決められる | A:親が初期値を持ち、子は required |
| 「未入力」の状態を親でも区別したい | B:default なし + 表示用の computed |
| 外部から届くデータが空のことがある | C:親で既定値を入れる |
| 親が v-model を付けずに使う部品(子だけで完結) | default 付きのままでよい(次の項) |
defaultが役立つ場面:v-modelなしでも動く部品
default 付きの DefaultCount を、v-model を付けずに単体でマウントしてボタンを2回押した結果です。
[v-model なし] 2回クリック → 子: 3
親が v-model を渡さなくても、子の中だけで値が更新されました。 「渡されれば親と同期し、渡されなければ自分で状態を持つ」部品を作りたいときに、default が活きます。
配列やオブジェクトのdefaultは関数で返す
<!-- src/TagList.vue -->
<script setup lang="ts">
// 配列やオブジェクトの default は関数で返す
const tags = defineModel<string[]>({ default: () => [] })
</script>
<template>
<button @click="tags = [...tags, `tag${tags.length + 1}`]">追加({{ tags.length }}件)</button>
</template>
親が ['a'] を渡した状態でボタンを押すと、親の値は ["a","tag2"] になりました。次の章の Mix.vue で試したところ、tags.push('x') では update イベントは発生せず、tags = [...tags, 'y'] と代入したときに発生しました。配列は新しい配列を代入して更新すると、変更が親にイベントとして届きます。
watchで監視する・名前あり/なしを併用する
defineModel の戻り値は ref なので、watch にそのまま渡せます。また、名前なし(modelValue)と名前付きは1つのコンポーネントで併用できます。
<!-- src/Mix.vue -->
<script setup lang="ts">
import { watch, ref } from 'vue'
const model = defineModel<string>({ required: true })
const title = defineModel<string>('title', { required: true })
const tags = defineModel<string[]>('tags', { default: () => [] })
const log = ref<string[]>([])
watch(model, (v, old) => log.value.push(`${old}->${v}`))
</script>
<template>
<div>
<input class="m" v-model="model" /><input class="t" v-model="title" />
<button class="push" @click="tags.push('x')">push</button>
<button class="assign" @click="tags = [...tags, 'y']">assign</button>
<p>{{ log.join(',') }}</p>
</div>
</template>
modelValue に a、title に t、tags に ['a'] を渡してマウントし、1つ目の入力欄に b、2つ目に u を入力、続けて push ボタン、assign ボタンの順に押した結果です。
push後 update:tags = null
代入後 update:tags = [[["a","x","y"]]]
a->b [["u"]]
| 確認できたこと | 出力 |
|---|---|
| watch が親の値の変化で動く | ログに a->b |
| 名前付きは update:title として送られる | [["u"]] |
| 配列の push ではイベントが発生しない | null |
| 配列を代入するとイベントが発生する | [[["a","x","y"]]] |
従来記法からdefineModelへ書き換える手順
Vue 3.4 以降であれば、子コンポーネント単位で次の順に置き換えられます。親は変更不要です。
| 手順 | 従来のコード | 書き換え後 |
|---|---|---|
| 1. props の modelValue を消す | defineProps<{ modelValue: string }>() |
const model = defineModel<string>({ required: true }) |
| 2. emits の update:modelValue を消す | defineEmits<{ 'update:modelValue': [v: string] }>() |
(不要) |
| 3. 読む場所を置き換える | props.modelValue |
model.value(template は model) |
| 4. 送る場所を置き換える | emit('update:modelValue', v) |
model.value = v |
| 5. input の結び付けを簡略化 | :value + @input |
v-model="model" |
| 6. 修飾子があれば分割代入に | props.modelModifiers?.xxx |
const [model, modifiers] = defineModel({ set }) |
| 7. デフォルト値を見直す | withDefaults の modelValue | 親に移すか、前章の A〜C で検討 |
modelValue 以外の props や emit が残る場合は、defineProps・defineEmits をそのまま併用できます。書き換え後に vue-tsc を走らせると、親から渡している値の型がずれていないかまで一度に確認できます。
書き換え時のチェックリスト
| 確認すること | 見るポイント |
|---|---|
| Vue の版が 3.4 以上か | package.json の vue |
| 親が必ず値を渡すか | 渡すなら required: true |
| default を付けた子に、親が undefined を渡していないか | マウント直後の親の値を確認 |
| ルート要素が1つか | 複数ルートにすると、親が付けた id や class の行き先が決まらず警告が出る(GetSet.vue は label で包んでいます) |
| 修飾子の名前を型引数に書いたか | つづり違いを vue-tsc が指摘してくれる |
| 配列・オブジェクトは代入で更新しているか | tags = [...tags, x] |
よくある質問
Q. defineModel と modelValue は、どちらを使えばよいですか? A. Vue 3.4 以降なら defineModel が簡潔です。中では modelValue と update:modelValue に展開されるので、親から見た動きは同じです。3.3 以前のプロジェクトでは、従来記法(props + emit)で書きます。
Q. defineModel の値を watch できますか?
A. 戻り値は ref なので、watch(model, (v) => …) のようにそのまま渡せます。computed の中で model.value を読むこともできます(FallbackCount.vue の shown がその例です)。
Q. 1つのコンポーネントに v-model はいくつまで付けられますか?
A. 名前を変えれば複数宣言できます。名前なしの defineModel()(modelValue)と、defineModel('title') のような名前付きを組み合わせることもできます。
Q. 修飾子の .trim と独自修飾子は一緒に使えますか? A. .trim と .number は、子に何も書かなくても親に届く値に反映されました。独自修飾子は、子の defineModel で分割代入の2つ目として受け取り、set の中で処理します。
まとめ
- defineModel は modelValue + update:modelValue を1行にまとめるマクロで、戻り値の ref を読み書きするだけで親と同期できる
- 親の書き方は変わらないので、子コンポーネント単位で書き換えられる
- 複数の v-model は
defineModel('名前')を並べ、親はv-model:名前で渡す - 独自修飾子は
const [model, modifiers] = defineModel({ set })で受け取り、set で変換する - 型指定は
defineModel<string>({ required: true })。required か default を付けると undefined が外れる - default を付けて親が undefined を渡すと、マウント直後は親 undefined・子 1 になる。初期値を親に持たせる、default を外して表示だけ補う、親で既定値を入れる、のいずれかでそろえる
- v-model なしでも動く部品を作りたいときは default が役立つ
参考・出典
- Vue.js 公式ドキュメント「コンポーネントの v-model」 https://ja.vuejs.org/guide/components/v-model
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vue defineModelの使い方|複数v-modelと修飾子をコードで解説」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。