TECHSCORE BLOG

クラウドCRMを提供するシナジーマーケティングのエンジニアブログです。

Next.js App Router の基礎:フロントエンドエンジニアのための入門ガイド(概念とディレクトリ構造編)

1. はじめに

1.1 App Router とは何か

Next.js 13.4 で正式リリースされた App Router は、従来の Pages Router に代わる新しいルーティングシステムです。最も重要な特徴は、React Server Components をデフォルトで採用している点にあります。これにより、サーバーサイドでコンポーネントをレンダリングし、必要な部分のみをクライアントに送信することで、パフォーマンスの大幅な向上を実現しています。

App Router は単なるファイル構成の変更ではありません。React 18 の新機能を最大限活用し、サーバーとクライアントの境界をより明確に定義することで、モダンな Web アプリケーション開発に最適化された仕組みです。

従来の SPA(Single Page Application)の利便性を保ちながら、初期読み込み速度などを大幅に改善できます。

1.2 Pages Router との基本的な違い

最も目に見える変化は、ルートファイルの配置場所です。Pages Router ではpagesディレクトリにファイルを配置 していましたが、App Router ではappディレクトリを使用します。しかし、真の違いはその背後にある設計思想にあります。

Pages Router ではすべてのコンポーネントがクライアントサイドで実行されていましたが、App Router では Server Components と Client Components を明確に区別します。Server Components はサーバー上で実行され、データベースアクセスなどのサーバーサイド処理を直接行えます。一方、Client Components は従来通りブラウザで実行され、インタラクティブな機能を担当します。

レンダリング方式も大きく変わりました。App Router では、デフォルトでサーバーサイドレンダリングが行われ、必要に応じてストリーミングレンダリングも利用できます。これにより、ページの一部が準備できた段階で順次表示し、ユーザー体験を向上させることが可能です。

1.3 この記事で学べる内容と対象読者

本記事は、Next.js の Pages Router での開発経験があり、App Router への移行を検討しているフロントエンドエンジニアを対象としています。React Hooks の基本的な使い方や、Next.js の基本概念(SSG、SSR、API Routes 等)について理解していることを前提として説明を進めます。

記事を通じて習得できるスキルは、App Router での実践的な開発能力です。具体的には、新しいファイル構成でのルーティング設定、Server ComponentsClient Components の使い分け、データフェッチングの新しい手法、Server Actions によるフォーム処理、そして実際のアプリケーション開発で必要となる実践的なテクニックを身につけることができます。

本記事は全 3 回のシリーズの第 1 回目にあたり、App Router の基礎的な概念とルーティングシステムを中心に扱います。第 2 回では、Server Components と Client Components の詳細な使い分けや、データ取得の実践的な手法について解説します。第 3 回では、Server Actions によるフォーム処理、メタデータ設定、認証の基礎など、より実用的なスキルを身につけていただく予定です。

最終的に、App Router を使用した Web アプリケーション開発の実務レベルの基礎をしっかりと固めることを目標としています。

2. App Router の基本構造

2.1 ディレクトリ構造の理解

App Router の核心は、フォルダとファイルがルート構造を定義し、各フォルダが URL セグメントにマップされるルートセグメントを表すという点にあります。Pages Router では pages ディレクトリにファイルを配置していましたが、App Router では app ディレクトリが中心となります。

この変更は単なるディレクトリ名の変更ではありません。App Router では、プロジェクトファイルを app ディレクトリ内のルートセグメント内に安全にコロケーションでき、誤ってルーティング可能になることはないという大きな利点があります。

つまり、コンポーネントファイルやユーティリティファイルを、関連するページと同じディレクトリに配置できるということです

基本的なディレクトリ構造は以下のようになります。

app/
├── layout.tsx      # ルートレイアウト(必須)
├── page.tsx        # トップページ
├── loading.tsx     # ローディングUI
├── error.tsx       # エラーUI
├── not-found.tsx   # 404ページ
├── global-error.tsx # グローバルエラーページ
└── globals.css     # グローバルスタイル

Pages Router との最も重要な違いは、ルート構造はフォルダを通じて定義されるが、page.tsx または route.tsx ファイルがルートセグメントに追加されるまでルートは公的にアクセスできないことです。

つまり、フォルダを作成しただけではルートは作成されず、必ず page.tsx ファイルが必要になります。

2.2 特別なファイルの役割

App Router では、特定の名前を持つファイルが特別な意味を持ちます。これらのファイルは、Next.js が自動的に認識し、適切な機能を提供します。

page.tsx: 実際に表示されるページ

page.tsx は React コンポーネントをエクスポートする特別な Next.js ファイルで、ルートがアクセス可能になるために必要です。このファイルがそのルートで実際に表示される UI を定義します。

// app/about/page.tsx
export default function About() {
  return (
    <div>
      <h1>About Page</h1>
      <p>Welcome to the About page of Synergy Marketing.</p>
    </div>
  );
}

layout.tsx: 共通レイアウト

レイアウトは複数のページ間で共有される UI で、ナビゲーション時に状態を保持し、インタラクティブなままで、再レンダリングされない特徴があります。

レイアウトコンポーネントは children プロパティを受け取り、ページまたは他のレイアウトをラップします。

// app/blog/layout.tsx
export default function BlogLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div>
      <nav>
        <h2>ブログナビゲーション</h2>
        {/* ナビゲーションコンテンツ */}
      </nav>
      <main>{children}</main>
    </div>
  );
}

loading.tsx: ローディング状態の UI

loading.tsx はオプションのファイルで、app ディレクトリ内の任意のディレクトリに作成でき、ページを React Suspense 境界内に自動的にラップする機能を提供します。

// app/blog/loading.tsx
export default function Loading() {
  return (
    <div className="loading-container">
      <div className="spinner"></div>
      <p>ブログ記事を読み込み中...</p>
    </div>
  );
}

ローディング UI は最初の読み込み時と兄弟ルート間をナビゲートする際に即座に表示される特徴があります。

error.tsx: エラーハンドリング

error.tsx はオプションのファイルで、エラーをアプリの最小可能な部分に分離し、このファイルを作成すると自動的にページを React エラー境界内にラップする機能を提供します。

// app/blog/error.tsx
"use client";

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>エラーが発生しました</h2>
      <p>{error.message}</p>
      <button onClick={() => reset()}>もう一度試す</button>
    </div>
  );
}

重要な点として、エラー境界は Client Components でなければならないため、ファイルの先頭に 'use client' ディレクティブが必要です。

not-found.tsx: 404 エラーページ

root の app/not-found.js ファイルは、予期された notFound() エラーをキャッチするだけでなく、アプリケーション全体の未一致 URL も処理する機能を持ちます。

// app/not-found.tsx
import Link from "next/link";

export default function NotFound() {
  return (
    <div>
      <h2>ページが見つかりません</h2>
      <p>要求されたリソースが見つかりませんでした。</p>
      <Link href="/">ホームに戻る</Link>
    </div>
  );
}

2.3 ルートレイアウトの必須要件

App Router において、ルートレイアウトは特別な役割を担います。app ディレクトリの最上位に定義されるこのレイアウトは必須であり、<html><body>タグを含まなければならないという重要な要件があります。

<html><body>タグの必要性

ルートレイアウトは <html><body> タグを定義する必要があり、ルートレイアウトに <title><meta> などの <head> タグを手動で追加すべきではないという制約があります。

代わりに Metadata API を使用する必要があります。

// app/layout.tsx
import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
  title: "My App",
  description: "Next.js App Router を使用したアプリケーション",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <header>
          <nav>{/* グローバルナビゲーション */}</nav>
        </header>
        {children}
        <footer>{/* グローバルフッター */}</footer>
      </body>
    </html>
  );
}

メタデータの設定方法

Metadata API を使用すべきで、これは自動的にストリーミングや <head> 要素の重複排除などの高度な要件を処理する機能を提供します。

静的なメタデータは metadata オブジェクトをエクスポートすることで設定できます。

import type { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    default: "My Blog",
    template: "%s | My Blog",
  },
  description: "Next.js で構築された技術ブログ",
  keywords: ["Next.js", "React", "TypeScript"],
};

テンプレート内の %s は特定のページタイトルに置き換えられるため、各ページで個別のタイトルを設定できます。

グローバルスタイルの適用

ルートレイアウトでは、アプリケーション全体に適用されるグローバルスタイルを import できます。

// app/layout.tsx
import "./globals.css"; // グローバルスタイルをインポート

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>{children}</body>
    </html>
  );
}

ルートレイアウトに追加した UI は、アプリケーション内のすべてのページで共有されるため、ナビゲーションやフッターなどの共通要素をここに配置することで、効率的にレイアウトを管理できます。

この基本構造を理解することで、App Router の柔軟性と powerful な機能を活用できるようになります。次章では、この構造を基盤としたルーティングの詳細な仕組みについて学習していきます。

3. ルーティングの基礎

3.1 基本的なページの作成方法

App Router でのページ作成は、フォルダとファイルの組み合わせによって行われます。Next.js はファイルシステムルーティングを使用し、フォルダがネストしたルートを作成するために使用され、各フォルダが URL セグメントにマップされるルートセグメントを表すという仕組みです。

フォルダとファイルの関係

page.tsx は React コンポーネントをエクスポートする特別な Next.js ファイルで、ルートがアクセス可能になるために必要です。つまり、フォルダを作成しただけではルートは作成されず、必ず page.tsx ファイルが必要になります。

// app/about/page.tsx
export default function About() {
  return (
    <div>
      <h1>About Page</h1>
      <p>Welcome to the About page of Synergy Marketing.</p>
    </div>
  );
}

URL パスとディレクトリ構造の対応

トップレベルの app/ ディレクトリがルートルート(/)として機能し、app ディレクトリの下に作成した任意のディレクトリが、URL セグメントにマップされる他のルートセグメントを作成するという対応関係があります。

具体的な例を見てみましょう。

app/
├── page.tsx                   # → /
├── about/
│   └── page.tsx               # → /about
├── blog/
│   └── page.tsx               # → /blog
└── contact/
    └── page.tsx               # → /contact

ネストしたルートの作成

ネストしたルートを作成するには、フォルダを互いにネストできる特徴があります。例えば、/blog/categories のようなルートを作成したい場合のようになります。

app/
└── blog/
    ├── page.tsx               # → /blog
    └── categories/
        └── page.tsx           # → /blog/categories
// app/blog/categories/page.tsx
export default function BlogCategories() {
  return (
    <div>
      <h1>ブログカテゴリ</h1>
      <ul>
        <li>技術</li>
        <li>ライフスタイル</li>
        <li>デザイン</li>
      </ul>
    </div>
  );
}

3.2 動的ルーティングの実装

動的ルーティングは、URL の一部が可変である場合に使用される機能です。動的セグメントは、フォルダの名前を角括弧で囲むことで作成できる[folderName]という記法で実装します。

[slug]形式の動的セグメント

ブログの個別記事ページなど、ID やスラッグに基づいてページを動的に生成する場合に使用します。

app/
└── blog/
    ├── page.tsx               # → /blog
    └── [slug]/
        └── page.tsx           # → /blog/[任意の文字列]

パラメータの取得方法(Next.js 15 対応)

重要:Next.js 15 での params 変更点

Next.js 15 では、paramsPromiseオブジェクトとして提供されるようになりました。後方互換性のため同期的なアクセスも現在は可能ですが、将来的には非推奨になる予定のため、新しい非同期での取得方法を採用することを推奨します。

// app/blog/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
};

export default async function BlogPost({ params }: Props) {
  // 推奨:非同期でのアクセス(将来も安全)
  const { slug } = await params;

  // slugを使用してブログ記事を取得
  const post = await fetchPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <p>スラッグ: {slug}</p>
      <div>{post.content}</div>
    </article>
  );
}

// データフェッチング関数の例
async function fetchPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`);
  if (!res.ok) {
    throw new Error("記事の取得に失敗しました");
  }
  return res.json();
}

現在利用可能な書き方の比較

// ✅ 推奨:非同期でのアクセス(将来も安全)
export default async function Page({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  return <div>My Post: {slug}</div>;
}

// ⚠️ 現在は動作するが将来非推奨:同期的アクセス
export default function Page({ params }: { params: { slug: string } }) {
  const { slug } = params; // 開発環境で警告が表示される可能性
  return <div>My Post: {slug}</div>;
}

キャッチオールルート

動的セグメントは、括弧内に省略記号を追加することで、後続のすべてのセグメントをキャッチするように拡張できる[...folderName]機能もあります。

app/
└── docs/
    └── [...slug]/
        └── page.tsx           # → /docs/a, /docs/a/b, /docs/a/b/c
// app/docs/[...slug]/page.tsx
type Props = {
  params: Promise<{ slug: string[] }>;
};

export default async function DocsPage({ params }: Props) {
  const { slug } = await params;

  return (
    <div>
      <h1>ドキュメント</h1>
      <p>パス: {slug.join("/")}</p>
      <p>セグメント数: {slug.length}</p>
    </div>
  );
}

キャッチオールルートでは、URL の複数のセグメントが配列として取得されます。

ルート 例 URL params
app/docs/[...slug]/page.tsx /docs/a { slug: ['a'] }
app/docs/[...slug]/page.tsx /docs/a/b { slug: ['a', 'b'] }
app/docs/[...slug]/page.tsx /docs/a/b/c { slug: ['a', 'b', 'c'] }

オプショナルキャッチオール

キャッチオールセグメントは、パラメータを二重角括弧に含めることでオプショナルにできる[[...folderName]]ため、より柔軟なルーティングが可能です。

app/
└── shop/
    └── [[...slug]]/
        └── page.tsx           # → /shop, /shop/clothes, /shop/clothes/tops
// app/shop/[[...slug]]/page.tsx
type Props = {
  params: Promise<{ slug?: string[] }>;
};

export default async function ShopPage({ params }: Props) {
  const { slug } = await params;

  if (!slug) {
    // /shop - ショップトップページ
    return <div>ショップトップページ</div>;
  }

  // /shop/clothes/tops など
  return (
    <div>
      <h1>ショップカテゴリ</h1>
      <p>カテゴリパス: {slug.join(" > ")}</p>
    </div>
  );
}

オプショナルキャッチオールの動作例

ルート 例 URL params
app/shop/[[...slug]]/page.tsx /shop { slug: undefined }
app/shop/[[...slug]]/page.tsx /shop/clothes { slug: ['clothes'] }
app/shop/[[...slug]]/page.tsx /shop/clothes/tops { slug: ['clothes', 'tops'] }

複数の動的セグメント

複数の動的セグメントを組み合わせることも可能です。

// app/[categoryId]/[itemId]/page.tsx
type Props = {
  params: Promise<{ categoryId: string; itemId: string }>;
};

export default async function ItemPage({ params }: Props) {
  const { categoryId, itemId } = await params;

  return (
    <div>
      <h1>商品詳細</h1>
      <p>カテゴリ: {categoryId}</p>
      <p>商品ID: {itemId}</p>
    </div>
  );
}

generateStaticParams との組み合わせ

動的ルーティングでは、ビルド時に静的ページを生成するためにgenerateStaticParams関数を使用できます。これにより、パフォーマンスを向上させることができます。

// app/blog/[slug]/page.tsx
type Props = {
  params: Promise<{ slug: string }>;
};

// ビルド時に静的生成するページを定義
export async function generateStaticParams() {
  const posts = await fetch("https://api.example.com/posts").then((res) =>
    res.json()
  );

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }));
}

export default async function BlogPost({ params }: Props) {
  const { slug } = await params;
  const post = await fetchPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <div>{post.content}</div>
    </article>
  );
}

TypeScript での型定義

動的ルーティングを使用する際の TypeScript 型定義の例

ルート params 型定義
app/blog/[slug]/page.tsx { slug: string }
app/shop/[...slug]/page.tsx { slug: string[] }
app/shop/[[...slug]]/page.tsx { slug?: string[] }
app/[categoryId]/[itemId]/page.tsx { categoryId: string, itemId: string }

移行時の推奨事項
Next.js 14 以前から移行する場合は、以下の点に注意してください。

段階的な移行が可能: 現在は同期的アクセスも動作するため、急いで変更する必要はありません
新規開発では非同期を採用: 将来性を考慮して、新しいコンポーネントでは async/await を使用
型定義の更新: TypeScript を使用している場合は、params の型を Promiseに更新
コードモッドの活用: Next.js 公式の@next/codemod ツールで自動変換も可能

この変更により、サーバーがリクエストを待つ前により多くの準備作業を行えるようになり、アプリケーション全体のパフォーマンスが向上します。現在は両方の書き方が可能ですが、将来性を考慮して非同期版での実装をお勧めします。

3.3 ネストしたレイアウトの活用

App Router の強力な機能の一つが、階層的なレイアウト構造です。レイアウトコンポーネントは children プロパティを受け取り、この children は別のページまたは別のレイアウトのいずれかになるという仕組みで、複雑な UI 構造を効率的に管理できます。

階層的なレイアウト構造

app/
├── layout.tsx                # ルートレイアウト
├── dashboard/
│   ├── layout.tsx            # ダッシュボードレイアウト
│   ├── page.tsx              # /dashboard
│   ├── analytics/
│   │   └── page.tsx          # /dashboard/analytics
│   └── settings/
│       └── page.tsx          # /dashboard/settings

レイアウトの継承の仕組み

レイアウトは階層的に適用されます。/dashboard/analytics にアクセスした場合、以下のような構造になります。

// app/layout.tsx (ルートレイアウト)
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body>
        <header>グローバルヘッダー</header>
        {children}
        <footer>グローバルフッター</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx (ダッシュボードレイアウト)
export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="dashboard">
      <aside>
        <nav>ダッシュボードナビゲーション</nav>
      </aside>
      <main>{children}</main>
    </div>
  );
}

レイアウトの利点

部分レンダリングとは、ナビゲーション時にクライアント上で変更されるルートセグメントのみが再レンダリングされ、共有セグメントは保持されることを意味します。これにより、パフォーマンスの向上とスムーズなユーザー体験を実現できます。

3.4 ルートグループとプライベートフォルダ

App Router では、URL に影響を与えずにルートを整理する方法が提供されています。

(group)によるルートグループ化

ルートグループを使用して Next.js アプリケーションを異なるセクションに分割できる機能があります。フォルダ名を括弧で囲むことで、URL パスに影響を与えずにルートをグループ化できます。

app/
├── (marketing)/
│   ├── layout.tsx            # マーケティング用レイアウト
│   ├── about/
│   │   └── page.tsx          # → /about
│   └── contact/
│       └── page.tsx          # → /contact
├── (shop)/
│   ├── layout.tsx            # ショップ用レイアウト
│   ├── products/
│   │   └── page.tsx          # → /products
│   └── cart/
│       └── page.tsx          # → /cart

複数のルートレイアウト

複数のルートレイアウトを作成するには、トップレベルの layout.tsx ファイルを削除し、各ルートグループ内に layout.tsx ファイルを追加することで、完全に異なる UI 体験を提供できます。

// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="marketing-theme">
        <nav>マーケティングナビ</nav>
        {children}
      </body>
    </html>
  );
}

// app/(shop)/layout.tsx
export default function ShopLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="ja">
      <body className="shop-theme">
        <nav>ショップナビ</nav>
        {children}
      </body>
    </html>
  );
}

_privateによるプライベートフォルダ

プライベートフォルダは、フォルダの前にアンダースコアを付けることで作成できる_folderName機能があります。これにより、ルーティングシステムから除外されるフォルダを作成できます。

app/
├── blog/
│   ├── page.tsx              # → /blog
│   ├── [slug]/
│   │   └── page.tsx          # → /blog/[slug]
│   └── _components/          # ルーティングから除外
│       ├── BlogCard.tsx
│       └── BlogList.tsx

ナビゲーションの実装

ルーティングが設定できたら、実際のナビゲーションを実装します。<Link> コンポーネントを使用してルート間をナビゲートできる機能が提供されています。

// コンポーネント内でのリンク作成
import Link from "next/link";

export default function Navigation() {
  return (
    <nav>
      <Link href="/">ホーム</Link>
      <Link href="/about">About</Link>
      <Link href="/blog">ブログ</Link>
      <Link href="/contact">お問い合わせ</Link>
    </nav>
  );
}

動的ルートへのリンクも簡単に作成できます。

// 動的ルートへのリンク
import Link from "next/link";

export default function BlogList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}

プログラマティックナビゲーション

ボタンクリックやフォーム送信後など、プログラマティックにナビゲーションを行いたい場合は、useRouter フックを使用してプログラマティックにルートを変更できる機能を活用します。

"use client";
import { useRouter } from "next/navigation";

export default function ContactForm() {
  const router = useRouter();

  const handleSubmit = async (formData: FormData) => {
    // フォーム送信処理
    await submitForm(formData);

    // 送信完了後にサンクスページに遷移
    router.push("/contact/thanks");
  };

  return (
    <form action={handleSubmit}>
      {/* フォーム要素 */}
      <button type="submit">送信</button>
    </form>
  );
}

App Router のルーティングシステムは、従来の Pages Router と比較して、より柔軟で直感的な設計になっています。フォルダ構造がそのまま URL 構造に対応し、レイアウトの階層化により効率的な UI 設計が可能になります。また、ルートグループやプライベートフォルダの機能により、プロジェクトの整理も容易になっています。

これらの基本的なルーティング機能を理解することで、複雑なアプリケーション構造も効率的に管理できるようになります。次章では、これらのルーティング機能と密接に関わる Server Components と Client Components の概念について詳しく学んでいきます。

4. まとめと次のステップ

本記事を通じて、App Router の核となる概念とルーティングシステムの基礎を学習しました。

習得した基礎知識

最も重要な変化は、React Server Components をデフォルトで採用している点です。これにより、サーバーサイドでコンポーネントをレンダリングし、必要な部分のみをクライアントに送信することで、パフォーマンスの大幅な向上を実現しています。

appディレクトリを中心とした新しい構造では、フォルダとファイルがルート構造を直接定義し、プロジェクトファイルを安全にコロケーションできます。特別なファイル(page.tsxlayout.tsxloading.tsxerror.tsx)の役割を理解し、適切に活用することで、効率的なアプリケーション構造を構築できます。

ルーティングシステムでは、ファイルシステムベースの直感的な設計と、動的ルーティング、ネストしたレイアウトの階層構造により、複雑なアプリケーションも効率的に管理できるようになりました。Next.js 15 ではparamsPromiseオブジェクトとして提供されるようになりましたが、後方互換性により段階的な移行が可能です。

次回に向けて

第 2 回では、今回学んだ基礎知識を活用して、Server Components と Client Components の詳細な使い分け、データフェッチングの手法、ナビゲーションの実装、メタデータの動的生成など、App Router 開発で必要となる知識を扱います。

今回学んだルーティングシステムの知識は、第 2 回で扱う Server/Client Components の概念と密接に関わります。どのコンポーネントをどの階層に配置するかという設計判断において、ルーティング構造の理解が重要になります。

参考資料

App Router は確かに学習コストがありますが、習得すれば間違いなく開発効率とアプリケーションパフォーマンスの両方が向上します。第 2 回では、App Router 開発に必要な知識をさらに深めていきましょう。

hiraokuのプロフィール画像
hiraoku

暇があったらクライミングしているフロントエンドエンジニアです。

シナジーマーケティング株式会社では一緒に働く仲間を募集しています。