Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

デスク環境2024

去年までのもの→デスク環境変遷まとめ (2020/5 ~ 2023/1) - Panda Noir

あとから見返す用に年1くらいでデスク環境を紹介したいなと思い立ったので書きます。あと、多分ウェブフロントエンドエンジニアとしては珍しく mac と windows の二刀流構成なので、他の人の参考にもなるかも。

デスク全景

まず写真をお見せするとこんな感じです。

デスク全景

光ったりしないし壁面収納とかしてないですが、まあ最低限いい感じになってるかなと。

デスク周辺機器

  • デスク: FLEXISPOT E8B
  • キーボード: Moonlander
  • マイク: blue Yeti X
  • ウェブカメラ: Elgato Facecam
  • ディスプレイ: BenQ EW2780Q WQHD
  • モニターアーム: エルゴトロン LX デスクマウントアーム

PC は2台あります(会社の mac と私物の windows PC)。あと、Switch もデスクに載ってます。家にテレビないため仕事机に載せざるを得ないです…

Moonlander 以外はそんな凝ったもの使ってるわけじゃないです。

工夫ポイント1: 配線が綺麗

配線がきれいというか、最近配線を綺麗にしました。

ディスプレイ裏にはHDMIケーブル2本、DisplayPortケーブル1本、ディスプレイの電源ケーブル1本、ウェブカメラのUSBケーブルがまとめられています。ほぼマジックバンドでゴリ押してまとめてる感じですが。ただ、これのおかげで正面からケーブルがほぼ見えません。

ディスプレイ裏

正面からはほぼケーブルが見えない

デスク下の配線も一応いい感じにしてます。正直普段見ないのであんまガチでこだわってはないです。というか配線を工夫しなければいけないほど接続するものがないです。

デスク下の配線

あとは私物のPCとSwitch周辺は最近かなり頑張って整理しました。コードがすっきりした状態になってて満足してます。

私物PC周辺はコードが結構整理されてる

工夫ポイント2: サイドに充電系のものをまとめた延長ケーブルを設置

充電ケーブル類

クランプで電源タップを机の横に固定してます。これがすこぶる便利です。たまに出社するときに充電器を持っていく必要があるんですが、そのとき充電器が机の下側にあると取り出すのが面倒です(実際面倒でした)。そこで、サイドにおいておくようにしたんですがこれがとにかく使いやすい。あと単純に普段使いもしやすいです。

工夫ポイント3: mac と windows の切り替えが簡単

このデスク最大のポイントは、mac と windows の二刀流のしやすさです。なんとボタン1つを押すだけで切り替え可能です。 ケーブルの抜き差しも一切必要ありません。

ディスプレイは mac、windows それぞれと常にケーブルで接続されていて、入力があったほうが自動的にディスプレイに表示されるため、映像に関してはなにもする必要なく切り替えできます。

マウスに関してはそもそも mac でマウスを使ってないので切り替える必要がありません。逆にマイクとウェブカメラは windows で使ってないので、こちらも切り替える必要ありません。

キーボードだけは接続先の切り替えが必要です。キーボードは USB switcher につないであり、USB switcher の切り替えボタンを押すことで mac と windows どちらに接続するか切り替えてます。

また、キーボードにはほかにも工夫点があります。それが mac 用キーレイアウト、windows 用キーレイアウトの2種類を作って切り替えているという点です。これによって、ctrl キーなどの位置を適切に設定してショートカットキーが mac と win でなるだけ揃うようにしています。このレイアウトカスタマイズ機能があるから moonlander を使っているといっても過言ではありません。

まとめ: 現環境最高!

現デスク環境、僕としてはかなり使いやすくて満足しています。ただディスプレイが映画観るにはちょい小さいのが不満なので今モニターの買い替えを検討中ですが。

サイドデスクとかサイドのワゴンも紹介できなくはないですが、その辺は別にPC関係ないのでいいかなと。

2023年を振り返る

去年の → 2022年を振り返る - Panda Noir

仕事、趣味、プライベートについて書くぞい。

仕事、技術

  • テックリード交代した
  • 実質チーム移動になった(多分社外秘なのでどのチームかは言えない)
  • 記事をいくつか書いてバズらせた

今年は仕事に関しては特筆することないかも。チームメンバー増えたしテックリード交代したからなるべく出しゃばらないようにひたすら気配を消すようにした+チームをほぼ移動した結果そうなった感じ。でもやってることはほとんど変わってない。

記事の執筆もいくつかした。zenn で書いて4本ほどバズらせることにも成功した。でもこれがmaxかも。来年は今年以上にバズるぞ!みたいな意気込みはいまのところない。

趣味

  • 映画ばっかり見てた(200本以上)
  • 趣味グラミングはあんまやらなかった。年始とかにまとまった時間取ったくらい。
  • 合気道は引っ越す前までは結構やってたが、引っ越ししてからいけなくなった……
  • ギターはぼちぼち
  • スプラ3にハマり中

今年は趣味はほぼ映画だったな。ほかは人間との交流が趣味みたいな感じだった。

あとはスプラか。これは結構やった。2ヶ月で100時間積むくらいにはやった。でもS帯入ってから全然勝てなくなっていまちょっとモチベ下がり中。立て直したい。

あ、あと趣味カウントじゃないけど、今年は月1冊以上は本を読んでて、平均すると月1.5冊読んでた。外に出るようになって電車時間が増えたからだな(電車のなかではなるだけスマホ見ないというルールを課してる)。これは意外と良かった。まあ読んだのはほぼ小説なんだけど。

その他プライベートとか

  • 引越しをした
  • 恋愛は2敗
  • 遊びに自分から誘えるようになった
  • ダイエットに成功した(7kgちょい)

今年の抱負として「交友」を掲げていて、それは結構達成できたかも。少なくとも去年よりは土日とかに人と会うようにできた。てか引っ越ししてからフットワーク軽くなった。俺にしては結構前進している。人は人間と交流しないといけないって事実に気づくのが遅すぎたけど、着実に成長してきてはいる。

恋愛は2敗。わからね〜〜〜〜〜。まあいいや。というか恋愛に向いてない気が結構してきている。

あすけんを使ってダイエットに取り組み3ヶ月で7kg程度落とした。腹筋割りたかったんだけど、そこまではいかなかった。3ヶ月以上のダイエットは精神に来るからできないな。今は普通に飯ガッツリ食って筋トレ中。

去年たてた目標について

去年たてた目標たち

  • 引っ越したい。今の家は来年で2年目だけど、更新はしないつもり。駅から遠いんじゃあ…人を呼ぶにしても不便。
  • 転職はまだしない。
  • 仕事関係はどうだろう…来年は何がしたいかな。最近は新しい技術とかは趣味の方でまかなえばいいかと思い始めているので仕事はこのままでいいかって思ってる。
  • 結婚圧がかかり始める年齢なのでそっちも急ぎたい。
  • 英語のリスニングを、そこそこのレベルまで上げたい

引っ越しはした。しっかりと便利になったし今のところ不満点がほぼゼロ。完全に快適。最高。

仕事に関しては「精神を安定させる」「タスクを無難にこなす」を目標にすすめて無事達成できたので、当初の目標どおりいけたと思う。今までの「ピーキーでもいいから成果を出す」スタイルから「突出しなくていいから安定して成果を出す」スタイルへのシフトがそこそこ成功した。

結婚云々はむしろ焦りが消えた。来年もぼちぼちやっていきたい。

英語のリスニングは今年ほとんど時間とれなかったな……

総評

メンタルだけ抜き出すと今年もかなり波があった。でも仕事は安定できた。これは結構成長(去年は失恋後にぶち凹んで仕事に影響を出して迷惑かけたため)。来年はそもそもメンタルがブレないように軸を作っていきたい。

仕事も安定してきている。良くも悪くも。でも来年はチーム移動から始まりそう。まだ見通し立ってなくて現時点では目標立てづらい。

恋愛はまあ去年よりはうまくなってきた(?)。容姿とかも写真見ると整ってきた感じがする。来年もぼちぼち頑張りたい。

趣味はまあ来年もこの調子だと映画になりそうだな…でも本も読みたいんだよなぁ。なんとかしたい

来年の目標とか

  • 仕事はとりあえずチーム移動後もうまく回したい。まずは安定させるところを目標に動きたい。
  • 恋愛はぼちぼちやっていきたい。
  • 英語のリスニングは来年こそちゃんとやりたいかもな…

Next.js で layout をネストしたときパフォーマンスはどうなるのか?

Next.js で作ったアプリの /foo にアクセスすると app/layout.tsx と app/foo/layout.tsx の両方が適用されます。

さて、このネストされた2つの layout それぞれでデータフェッチをしていた場合、どうなるでしょうか?

  • app/layout.tsx のフェッチ完了後に app/foo/layout.tsx のフェッチが始まる
  • app/foo/layout.tsx のフェッチ完了後に app/layout.tsx のフェッチが始まる
  • 同時に app/layout.tsx のフェッチと app/foo/layout.tsx のフェッチが始まる

ぱっと聞かれて分かりますか?僕は分からなかったので リポジトリ を実際に作って検証してみました。

答え: 同時に app/layout.tsx のフェッチと app/foo/layout.tsx のフェッチが始まる

答えは 「同時に始まる」 です。 検証に使ったリポジトリ

実例

以下のコードを実行したときにかかる時間は max(getSession() の時間, fetchUserData() の時間) になります。getSession() の時間 + fetchUserData() の時間 ではありません。

// app/layout.tsx
import { setTimeout } from 'timers/promises';
import { getSession } from '@auth/nextjs-auth0';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const user = await getSession();
  return (
    <html lang="en">
      <body>
        <h1>hello {user.name}</h1>
        <div>{children}</div>
      </body>
    </html>
  );
}
import { setTimeout } from 'timers/promises';
import { fetchUserData } from './fetchUserData';

export default async function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  const data = await fetchUserData();
  return (
    <div>
      <h2>data: {data}</h2>
      <div>{children}</div>
    </div>
  );
}

Next.js + Chakra UI で初期描画からダークモードを反映させたい

問題: なにも設定しないと初期描画のときにカラーモードが反映されない

demo

このように、Chakra UI でなにも設定しないとリロード直後に一瞬ライトモードで表示されます。この記事ではこの問題を解消する方法を紹介します。

tl;dr

ColorModeScript を追加する + initialColorMode をセットする

'use client';
export const ChakraProviderClient = ({
  children,
  initialColorMode,
}: PropsWithChildren<{
  initialColorMode: 'light' | 'dark';
}>) => (
  <ChakraProvider
    colorModeManager={cookieStorageManager}
    theme={extendTheme({ config: { initialColorMode } })}
  >
    <ColorModeScript type="cookie" />
    {children}
  </ChakraProvider>
);

対処1: ColorModeScript を追加する

demo

ColorModeScript をセットすると、最初から背景色や文字色が設定されるようになります。 (コンポーネントの色は一瞬ライトモードで表示されていますが、これは後述の initialColorMode で直ります)

ColorModeScript はこのように使います。

<ColorModeScript type="cookie" />

<ColorModeScript type="cookie" /> はページ読み込み時に cookie を見て body に light/dark クラスを追加してくれます(ColorModeScript の実装)。これによって背景色などがうまく機能するようになります。

対処2: initialColorMode をセットする

initialColorMode をセットすると、リロード時にもボタンの色が設定したカラーモードで表示されるようになります。(ColorModeScript との効果を比較するために、動画ではColorModeScript は抜いています)

demo

initialColorMode をセットしないと、サーバーレンダリング時点では設定で決まった色でレンダリングしてしまいます。そのため、設定したカラーモードで描画するために intialColorMode をセットするようにします。

<ChakraProvider
  colorModeManager={cookieStorageManager}
  theme={extendTheme({ config: { initialColorMode } })}
>
  {children}
</ChakraProvider>

intialColorMode はサーバーサイドで cookies().get('chakra-ui-color-mode')?.value で取得できます。あとはこれを ChakraProvider に渡せば完了…なのですが、ここで一つ注意すべき点があります。それは、Client component 内でないと theme={extendTheme({ config: { initialColorMode } })} を直接渡せないということです。これは、extendTheme で返されるオブジェクトのプロパティに関数が混ざっていて「クライアントコンポーネントに直接関数を渡せない」という制限に引っかかるのが原因です。

これに引っかからないように、クライアントコンポーネントの中で ChakraProvider に theme を渡すようにしてください。

完成形

以上をまとめるとこのようになります。

// ChakraProviderClient.tsx
'use client';
export const ChakraProviderClient = ({
  children,
  initialColorMode,
}: PropsWithChildren<{
  initialColorMode: 'light' | 'dark';
}>) => (
  <ChakraProvider
    colorModeManager={cookieStorageManager}
    theme={extendTheme({ config: { initialColorMode } })}
  >
    <ColorModeScript type="cookie" />
    {children}
  </ChakraProvider>
);
// layout.tsx
import { cookies } from 'next/headers';
import { ChakraProviderClient } from './ChakraProviderClient';

export default function Layout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body>
        <ChakraProviderClient initialColorMode={cookies().get('chakra-ui-color-mode')?.value}>
          {children}
        </ChakraProviderClient>
      </body>
    </html>
  );
}

demo

一応動画も撮りましたが、表示が一切変わらないのでわかりづらい…

逆FizzBuzzを解いてみた2

全然書いた記憶はなかったんですが、昔の僕は逆FizzBuzz という問題に挑戦してました。 逆FizzBuzzを解いてみた - Panda Noir

この解答コードがもうちょいシンプルに書けたので載せておきます

おさらい: 逆FizzBuzzとは?

  • [1,2,3,4,5,…] のように並んだ数列がある
  • これを FizzBuzz で変換する([1,2,Fizz,4,Buzz,…])
  • そこから数字を取り除く([Fizz,Buzz,…])
  • そうして出来た文字列が入力として与えられる
  • このとき元の数列を求めよ
  • ただし、元の数列は複数ありうるため、数列の最初の数字が一番小さい数列を正解とする

以上が逆FizzBuzzです。

サンプルテストケース

  • 入力が Fizz であれば [3] が正解
  • 入力が Buzz Fizz Fizz Buzz Fizz なら [5, 6, 7, 8, 9, 10, 11, 12] が正解

Fizz に対して [6] と出力するのは、[3] のほうが数字が小さいため不正解。

コード

だいたいは元記事の方針に沿ってリファクタリングしました。

const range = (start, stop) =>
  stop < start ? [] : [...Array(stop - start).keys()].map((x) => x + start);

const solve = (arr) => {
  arr = arr.map((s) => s.replace(/[a-z]/g, ''));
  const index = arr.indexOf('FB');
  if (index === -1) {
    // FB が含まれていない
    switch (arr.join('')) {
      case 'F': return range(3, 4);
      case 'B': return range(5, 6);
      case 'FB': return range(9, 11);
      case 'BF': return range(5, 7);
      case 'FF': return range(6, 10);
      case 'FBF': return range(3, 7);
      case 'BFF': return range(5, 10);
      case 'FFB': return range(6, 11);
      case 'FBFF': return range(3, 10);
      case 'BFFB': return range(5, 11);
      case 'FFBF': return range(6, 13);
      case 'FBFFB': return range(3, 11);
      case 'BFFBF': return range(5, 13);
      case 'FBFFBF': return range(3, 13);
      default: return null;
    }
  }

  const beforeFB = arr.slice(0, index);
  const afterFB = arr.slice(index + 1);
  // arr == [...beforeFB, 'FB', ...afterFB]

  // FB 以降が規則通りに並んでいるかチェックする
  if (!afterFB.every((s, i) => s === 'F B F F B F FB'.split(' ')[i % 7])) {
    return null;
  }

  const start = { '': 15, F: 12, BF: 10, FBF: 9, FFBF: 6, BFFBF: 5, FBFFBF: 3 }[
    beforeFB.join('')
  ];
  if (!start) {
    return null;
  }
  return range(
    start,
    (Math.floor(afterFB.length / 7) + 1) * 15 +
      [1, 4, 6, 7, 10, 11, 13][afterFB.length % 7]
  );
};

ちゃんとテストケースを確認したわけじゃないので間違ってるかも。一応以下のケースは満たしていることを確認済み。

検証に使ったテストケース

for (const [input, expect] of [
  ['Fizz', [3]],
  ['Buzz', [5]],
  ['FizzBuzz', [15]],
  ['Fizz Fizz', [6, 7, 8, 9]],
  ['Fizz Buzz', [9, 10]],
  ['Buzz Fizz', [5, 6]],
  ['Fizz FizzBuzz', [12, 13, 14, 15]],
  ['FizzBuzz Fizz', [15, 16, 17, 18]],
  ['Fizz Fizz Buzz', [6, 7, 8, 9, 10]],
  ['Fizz Buzz Fizz', [3, 4, 5, 6]],
  ['Buzz Fizz Fizz', [5, 6, 7, 8, 9]],
  ['Buzz Fizz FizzBuzz', [10, 11, 12, 13, 14, 15]],
  ['Fizz Fizz Buzz Fizz', [6, 7, 8, 9, 10, 11, 12]],
  ['Fizz Buzz Fizz Fizz', [3, 4, 5, 6, 7, 8, 9]],
  ['Buzz Fizz Fizz Buzz', [5, 6, 7, 8, 9, 10]],
  ['Fizz Buzz Fizz FizzBuzz', [9, 10, 11, 12, 13, 14, 15]],
  ['Fizz Buzz Fizz Fizz Buzz', [3, 4, 5, 6, 7, 8, 9, 10]],
  ['Buzz Fizz Fizz Buzz Fizz', [5, 6, 7, 8, 9, 10, 11, 12]],
  ['Fizz Fizz Buzz Fizz FizzBuzz', [6, 7, 8, 9, 10, 11, 12, 13, 14, 15]],
  ['Fizz Buzz Fizz Fizz Buzz Fizz', [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]],
  [
    'Buzz Fizz Fizz Buzz Fizz FizzBuzz',
    [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  ],
  [
    'Fizz Buzz Fizz Fizz Buzz Fizz FizzBuzz',
    [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
  ],
]) {
  console.log(
    JSON.stringify(solve(input.split(' '))) === JSON.stringify(expect)
  );
}