Electronでスクリプトを書いていたところ、Node.jsのAPIとDOM要素を結びつけるのに迷いました。
main.jsではDOM要素にアクセスできず、HTMLから読み込んだスクリプト(renderer.js)ではNode.jsのAPIにアクセスできないからです。
「プロセスモデル | Electron」をもとに、それぞれの役割の違いを見てみましょう。

以前バージョンでは、レンダラープロセスから直接Node.js APIが利用できましたが、セキュリティのために制限されるようになりました。
1. 3つのスクリプトと2つのプロセス
見様見真似でスクリプトを編集していると、思うように requireができなかったりして混乱しました。

Uncaught ReferenceError: require is not defined at renderer.js
このようなReferenceError(参照エラー)が発生するのは、スクリプトによって、動作するプロセス・コンテクストが異なります。
Electronのサンプルをみると、3種類のJavaScriptファイルがあります。

1-1. メインプロセスとレンダラープロセス
Electronアプリは、メインプロセスとレンダラープロセスに分かれています。これは、Chromeブラウザの設計に由来します。

以前のブラウザでは、これらの機能を単一のプロセスで実現していました。 このやり方は(…)、1 つのウェブサイトがクラッシュしたりハングアップしたりすると、ブラウザ全体に影響が及びます。
この問題を解決するため、Chrome チームは各タブがそれぞれのプロセスで描画するようにすると決め、ウェブページ上のバグや悪意のあるコードがアプリ全体に与える影響を制限することにしました。 単一のブラウザプロセスはこれらのプロセスを制御し、アプリケーションのライフサイクル全体を制御します。
Electron アプリ開発者の場合、メインとレンダラーの 2 種類のプロセスを制御します。 これらは、上述の Chrome 独自のブラウザプロセスとレンダラープロセスと似ています。
プロセスモデル | Electron
メインプロセスは、サーバサイド、
レンダラープロセスは、クライアントサイドのスクリプトです。
以前のElectronのバージョンでは、レンダラープロセスからもNode.js環境を生成できましたが、現在は無効になっています。「クロス・サイト・スクリプティング」の脆弱性になる恐れがあるからです。
注意: 開発を容易にするために、レンダラープロセスを完全な Node.js 環境で生成できます。 歴史的には、これがデフォルトでしたが、セキュリティ上の理由からこの機能は無効になりました。
プロセスモデル | Electron

このため、少し古い情報を頼りにコードを書くと、「なぜか動作しない」ということになります。
2. メインプロセス
Electron アプリのメインプロセスは一つです。
Node.js環境で動作するということは、
・モジュールを require
したり
・Node.js のすべての API を利用したりできます。
メインプロセスのスクリプトは、package.jsonで指定します。
3. レンダラープロセス
レンダラープロセスは、メインプロセスがウインドウを作成したときに開始し、対応するBrowserWindow
インスタンスが破棄されると終了します。
レンダラープロセスのスクリプトは、HTML内の<script>要素で直接記述するか、読み込むかして動作します。
外部スクリプトを読み込む可能性があるため、require
やその他 Node.js の API は制限されています。
3-1. BrowserWindow::webContents
メインプロセス側からウェブコンテンツを操作するには、ウインドウの webContents
オブジェクトを使います。

4. プリロードスクリプト
プリロードスクリプトは、レンダラープロセス内でコンテンツ読込み前に実行されます。
プリロードスクリプトの主な役割は、レンダラースクリプトからNode.js API にアクセスできるように、contextBridge
モジュールで公開することです。
4-1. webPreference.preload
プリロードスクリプトは、メインプロセスでウィンドウを生成するときに、webPreferences
オプションで関連付けられます。

プリロードスクリプトは、レンダラーのコンテキスト内で実行されます。しかし、HTMLではなくメインプロセスで指定されたスクリプトなので、特別に Node.js の API にアクセスできる権限が与えられています。
4-2. contextBridge.exposeInMainWorld
デフォルトでは、プリロードスクリプトの特権的 API は、レンダラーのメインワールドから分離されています。これは、「コンテキスト分離 (contextIsolation)」という仕組みで、外部ウェブコンテンツがAPIを悪用するのを防いでいます。
プリロードスクリプトで、contextBridge.exposeInMainWorldメソッドを使うと、windowを通して値や関数を公開できます。

例えば、レンダラーからでメインプロセスにプロセス間通信 (IPC)が必要なら、ipcRenderer
ヘルパーをレンダラーに公開すればよいわけです。

こちらもどうぞ