Tuesday, June 17, 2025

pre-commit으로 팀의 코드 품질 자동화하기

개발자라면 누구나 한 번쯤은 커밋 메시지에 "Fix typo"나 "Apply linter" 같은 내용을 적어본 경험이 있을 겁니다. 이런 사소한 실수는 코드 리뷰 과정에서 불필요한 시간을 소모하게 하고, 팀 전체의 생산성을 저하시키는 원인이 되기도 합니다. 만약 이런 실수를 커밋하기 전에 자동으로 바로잡을 수 있다면 어떨까요? 바로 이 지점에서 Git Hook, 특히 pre-commit hook이 강력한 해결책으로 등장합니다.

이 글에서는 Git Hook의 기본 개념부터 시작하여, 팀 단위 프로젝트에서 코드 품질을 일관되게 유지하고 개발 워크플로우를 혁신적으로 개선할 수 있는 pre-commit 프레임워크의 설정 및 활용법을 상세히 다룹니다.

Git Hook이란 무엇인가?

Git Hook은 Git의 특정 이벤트(예: 커밋, 푸시)가 발생했을 때 자동으로 실행되는 스크립트입니다. 이를 통해 개발자는 특정 조건이 충족되지 않았을 때 커밋을 막거나, 커밋 메시지 형식을 강제하거나, 테스트를 자동으로 실행하는 등 다양한 자동화 작업을 수행할 수 있습니다.

Git Hook 스크립트는 모든 Git 저장소의 .git/hooks/ 디렉토리 안에 위치합니다. git init으로 새로운 저장소를 생성하면, 이 디렉토리 안에 다양한 샘플 훅(.sample 확장자)들이 생성된 것을 볼 수 있습니다.

$ ls .git/hooks/
applypatch-msg.sample         pre-commit.sample           pre-rebase.sample
commit-msg.sample             pre-merge-commit.sample     pre-receive.sample
fsmonitor-watchman.sample     pre-push.sample             update.sample
post-update.sample            prepare-commit-msg.sample

이 샘플 파일들 중 하나의 확장자에서 .sample을 제거하고 실행 권한을 부여하면 해당 훅이 활성화됩니다. 예를 들어, pre-commit.sample 파일의 이름을 pre-commit으로 바꾸고 실행 권한을 주면, git commit 명령을 실행하기 직전에 해당 스크립트가 실행됩니다.

가장 강력한 훅: pre-commit

수많은 훅 중에서도 pre-commit은 가장 널리 사용되고 강력한 훅 중 하나입니다. 커밋이 실제로 생성되기 직전에 실행되기 때문에, 코드 품질과 관련된 거의 모든 검사를 이 단계에서 수행할 수 있습니다.

  • 코드 스타일 검사 (Linting): 코드가 팀의 코딩 컨벤션을 따르는지 확인합니다.
  • 코드 포맷팅 (Formatting): 정해진 규칙에 따라 코드 스타일을 자동으로 수정합니다.
  • 비밀 키 및 민감 정보 유출 방지: 커밋에 실수로 포함된 API 키나 비밀번호를 찾아냅니다.
  • 디버깅 코드 방지: console.logdebugger 같은 코드가 커밋되는 것을 막습니다.
  • 단위 테스트 실행: 커밋하려는 코드가 기존 테스트를 통과하는지 빠르게 확인합니다.

전통적인 Git Hook 방식의 한계

.git/hooks/ 디렉토리에 직접 셸 스크립트를 작성하는 방식은 간단하지만 팀 프로젝트에서는 몇 가지 치명적인 단점이 있습니다.

  1. 버전 관리가 안 된다: .git 디렉토리는 Git의 추적 대상이 아니므로, 훅 스크립트를 팀원들과 공유하고 버전을 관리하기가 매우 어렵습니다.
  2. 설정이 번거롭다: 새로운 팀원이 프로젝트에 합류할 때마다 수동으로 훅 스크립트를 설정하고 실행 권한을 부여해야 합니다.
  3. 다양한 언어 환경 지원의 어려움: 파이썬, 자바스크립트, 자바 등 여러 언어를 사용하는 프로젝트에서는 각 언어에 맞는 린터와 포맷터를 설정하고 관리하는 것이 복잡해집니다.

이러한 문제들을 해결하기 위해 등장한 것이 바로 pre-commit 프레임워크입니다.

pre-commit 프레임워크로 스마트하게 관리하기

pre-commit은 Python으로 만들어진 Git Hook 관리 프레임워크입니다. 이 프레임워크는 .pre-commit-config.yaml이라는 설정 파일을 통해 훅을 정의하고 관리합니다. 이 파일은 프로젝트 루트에 위치하여 버전 관리가 가능하므로, 팀원 모두가 동일한 훅 설정을 공유할 수 있습니다.

1. 설치 및 초기 설정

먼저, pre-commit을 설치합니다. Python 패키지 매니저인 pip를 사용하는 것이 일반적입니다.

# pip를 사용하여 설치
pip install pre-commit

# Homebrew (macOS)를 사용하여 설치
brew install pre-commit

설치가 완료되면, 프로젝트 루트 디렉토리에 .pre-commit-config.yaml 파일을 생성합니다. 이 파일에 우리가 사용할 훅들을 정의합니다.

다음은 기본적인 설정 파일 예시입니다.

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0 # 항상 최신 안정 버전을 사용하는 것이 좋습니다.
    hooks:
    -   id: trailing-whitespace # 파일 끝의 공백 제거
    -   id: end-of-file-fixer # 파일 끝에 개행 문자 추가
    -   id: check-yaml # YAML 파일 문법 검사
    -   id: check-added-large-files # 대용량 파일이 추가되는 것을 방지

설정 파일 작성이 끝났다면, 다음 명령어를 실행하여 Git Hook을 .git/hooks/pre-commit에 설치합니다. 이 과정은 프로젝트를 처음 클론받았을 때 한 번만 실행하면 됩니다.

pre-commit install

이제 git commit을 시도하면, pre-commit이 스테이징된 파일들에 대해 설정된 훅들을 자동으로 실행합니다.

2. 다양한 언어를 위한 훅 추가하기

pre-commit의 진정한 강력함은 다양한 언어와 도구를 손쉽게 통합할 수 있다는 점에서 나옵니다. 예를 들어, Python 프로젝트에서는 black(포맷터)과 ruff(린터), JavaScript 프로젝트에서는 prettier(포맷터)와 eslint(린터)를 추가할 수 있습니다.

Python 프로젝트 예시 (black, ruff)

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
-   repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
    -   id: black
-   repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
    -   id: ruff
        args: [--fix] # 자동으로 수정 가능한 오류는 수정
    -   id: ruff-format

JavaScript/TypeScript 프로젝트 예시 (prettier, eslint)

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
-   repo: https://github.com/prettier/prettier
    rev: 3.2.5
    hooks:
    -   id: prettier
        # 추가적인 인자를 전달하여 특정 파일 타입에만 적용 가능
        # types: [javascript, typescript, css, markdown]
-   repo: local # 로컬에 설치된 eslint를 사용하는 경우
    hooks:
    -   id: eslint
        name: eslint
        entry: npx eslint --fix
        language: node
        types: [javascript, typescript]
        # 초기 실행 속도를 위해 항상 실행되도록 설정
        always_run: true
        # 스테이징된 파일만 인자로 전달
        pass_filenames: false

repo: local을 사용하면, package.json에 명시된 버전의 도구를 사용할 수 있어 팀원 간의 도구 버전 불일치 문제를 해결할 수 있습니다.

3. 실제 워크플로우

이제 모든 설정이 완료되었습니다. 개발자가 코드를 수정한 후 커밋을 시도하면 어떤 일이 벌어질까요?

  1. 개발자가 파일을 수정하고 git add . 명령으로 스테이징합니다.
  2. git commit -m "피처 추가" 명령을 실행합니다.
  3. pre-commit이 자동으로 실행되어 .pre-commit-config.yaml에 정의된 훅들을 스테이징된 파일에 대해 순차적으로 실행합니다.
  4. 성공 시나리오: 모든 훅이 성공적으로 통과하면, 커밋이 정상적으로 완료됩니다.
    $ git commit -m "새로운 기능 추가"
    Trim Trailing Whitespace........................................Passed
    Fix End of Files................................................Passed
    Check Yaml......................................................Passed
    black...........................................................Passed
    ruff............................................................Passed
    [feature/new-logic 1a2b3c4] 새로운 기능 추가
     2 files changed, 15 insertions(+)
    
  5. 실패 시나리오: 하나 이상의 훅이 실패하면(예: 린팅 오류 발견), pre-commit은 해당 오류를 출력하고 커밋을 중단시킵니다.
    $ git commit -m "버그 수정"
    Trim Trailing Whitespace........................................Passed
    Fix End of Files................................................Passed
    black...........................................................Failed
    - hook id: black
    - files were modified by this hook
    
    reformatted my_bad_file.py
    
    All done! ✨ 🍰 ✨
    1 file reformatted.
    

    이 경우, black이나 prettier와 같이 자동 수정 기능이 있는 훅은 파일을 직접 수정합니다. 개발자는 수정된 파일을 다시 스테이징(git add my_bad_file.py)하고 다시 커밋을 시도하면 됩니다. 이 과정을 통해 지저분한 "Fix lint" 커밋 없이 항상 깔끔한 코드를 유지할 수 있습니다.

결론: 왜 pre-commit을 도입해야 하는가?

pre-commit 프레임워크를 도입하는 것은 단순한 도구 추가를 넘어, 개발 문화 자체를 개선하는 효과적인 방법입니다.

  • 일관성 있는 코드 품질: 모든 팀원이 동일한 규칙에 따라 코드를 작성하고 검사하므로, 프로젝트 전체의 코드 품질이 상향 평준화됩니다.
  • 리뷰 시간 단축: 코드 리뷰어는 스타일이나 사소한 오류 대신 비즈니스 로직에 더 집중할 수 있습니다.
  • 자동화된 워크플로우: 개발자는 린팅이나 포맷팅 같은 반복적인 작업을 신경 쓸 필요 없이 개발에만 집중할 수 있습니다.
  • 실수 방지: 민감 정보나 디버깅 코드가 저장소에 커밋되는 것을 사전에 차단하여 보안을 강화합니다.

처음에는 설정을 추가하고 팀원들에게 사용법을 안내하는 약간의 노력이 필요할 수 있습니다. 하지만 이 작은 투자는 장기적으로 팀의 생산성을 극대화하고, 더 견고하고 유지보수하기 쉬운 코드를 만드는 밑거름이 될 것입니다. 지금 바로 여러분의 프로젝트에 pre-commit을 도입하여 자동화된 코드 품질 관리의 힘을 경험해 보세요.

Automating Code Quality: A Practical Guide to Git Pre-Commit Hooks

If you're a developer, you've likely made commits with messages like "Fix typo" or "Apply linter." These minor mistakes can consume unnecessary time during code reviews and hinder the entire team's productivity. What if you could automatically fix these issues before they are even committed? This is where Git Hooks, and specifically the pre-commit hook, emerge as a powerful solution.

This article will cover everything from the basic concepts of Git Hooks to a detailed guide on setting up and using the pre-commit framework to consistently maintain code quality and revolutionize your development workflow in a team environment.

What Are Git Hooks?

Git Hooks are scripts that run automatically when a specific Git event occurs, such as a commit or a push. They allow developers to perform various automated tasks, like preventing a commit if certain conditions aren't met, enforcing a commit message format, or automatically running tests.

Git Hook scripts are located in the .git/hooks/ directory of every Git repository. When you create a new repository with git init, you'll find this directory populated with various sample hooks (with a .sample extension).

$ ls .git/hooks/
applypatch-msg.sample         pre-commit.sample           pre-rebase.sample
commit-msg.sample             pre-merge-commit.sample     pre-receive.sample
fsmonitor-watchman.sample     pre-push.sample             update.sample
post-update.sample            prepare-commit-msg.sample

To activate a hook, you simply remove the .sample extension from one of these files and make it executable. For example, if you rename pre-commit.sample to pre-commit and grant it execute permissions, that script will run just before you execute a git commit command.

The Most Powerful Hook: pre-commit

Among the many hooks available, pre-commit is one of the most widely used and powerful. Because it runs just before a commit is actually created, it's the perfect stage to perform nearly any check related to code quality.

  • Code Style Checking (Linting): Ensures that code adheres to the team's coding conventions.
  • Code Formatting: Automatically reformats code according to a predefined set of rules.
  • Preventing Secret Leaks: Detects API keys or passwords accidentally included in a commit.
  • Blocking Debug Code: Prevents code like console.log or debugger from being committed.
  • Running Unit Tests: Quickly verifies that the code being committed passes existing tests.

The Limitations of the Traditional Git Hook Approach

While writing shell scripts directly in the .git/hooks/ directory is straightforward, it has several critical drawbacks for team projects.

  1. Not Version-Controlled: The .git directory is not tracked by Git, making it extremely difficult to share and version-control hook scripts with team members.
  2. Cumbersome Setup: Every time a new team member joins the project, they must manually set up the hook scripts and grant execute permissions.
  3. Difficulty Supporting Multi-Language Environments: In projects using multiple languages like Python, JavaScript, and Java, setting up and managing the appropriate linters and formatters for each language becomes complex.

The pre-commit framework was created to solve these very problems.

Smart Management with the pre-commit Framework

pre-commit is a Git Hook management framework built in Python. It defines and manages hooks through a configuration file named .pre-commit-config.yaml. This file is placed in the project root, making it version-controllable, so every team member can share the exact same hook configuration.

1. Installation and Initial Setup

First, install pre-commit. Using the Python package manager, pip, is the most common method.

# Install using pip
pip install pre-commit

# Install using Homebrew (macOS)
brew install pre-commit

Once installed, create a .pre-commit-config.yaml file in your project's root directory. This file will define the hooks we want to use.

Here is an example of a basic configuration file:

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0 # It's always a good idea to use the latest stable version.
    hooks:
    -   id: trailing-whitespace # Trims trailing whitespace.
    -   id: end-of-file-fixer # Ensures a file is either empty or ends with a newline.
    -   id: check-yaml # Checks yaml files for parseable syntax.
    -   id: check-added-large-files # Prevents giant files from being committed.

After creating the configuration file, run the following command to install the Git hook into .git/hooks/pre-commit. This step only needs to be done once when you first clone the project.

pre-commit install

Now, when you attempt to git commit, pre-commit will automatically run the configured hooks on the staged files.

2. Adding Hooks for Different Languages

The true power of pre-commit lies in its ability to easily integrate with various languages and tools. For instance, you can add black (formatter) and ruff (linter) for a Python project, or prettier (formatter) and eslint (linter) for a JavaScript project.

Example for a Python Project (black, ruff)

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
-   repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
    -   id: black
-   repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
    -   id: ruff
        args: [--fix] # Automatically fix what can be fixed.
    -   id: ruff-format

Example for a JavaScript/TypeScript Project (prettier, eslint)

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
-   repo: https://github.com/prettier/prettier
    rev: 3.2.5
    hooks:
    -   id: prettier
        # You can pass additional arguments to target specific file types
        # types: [javascript, typescript, css, markdown]
-   repo: local # Use locally installed eslint
    hooks:
    -   id: eslint
        name: eslint
        entry: npx eslint --fix
        language: node
        types: [javascript, typescript]
        # To improve initial run speed, set it to always run
        always_run: true
        # Pass only staged files as arguments
        pass_filenames: false

Using repo: local allows you to use the tool versions specified in your package.json, resolving tool version mismatch issues among team members.

3. The Actual Workflow

Now that everything is set up, what happens when a developer tries to commit their code?

  1. A developer modifies files and stages them with git add ..
  2. They run the command git commit -m "Add new feature".
  3. pre-commit runs automatically, executing the hooks defined in .pre-commit-config.yaml sequentially on the staged files.
  4. Success Scenario: If all hooks pass successfully, the commit is completed normally.
    $ git commit -m "Add new feature"
    Trim Trailing Whitespace........................................Passed
    Fix End of Files................................................Passed
    Check Yaml......................................................Passed
    black...........................................................Passed
    ruff............................................................Passed
    [feature/new-logic 1a2b3c4] Add new feature
     2 files changed, 15 insertions(+)
    
  5. Failure Scenario: If one or more hooks fail (e.g., a linting error is found), pre-commit will print the error and abort the commit.
    $ git commit -m "Fix bug"
    Trim Trailing Whitespace........................................Passed
    Fix End of Files................................................Passed
    black...........................................................Failed
    - hook id: black
    - files were modified by this hook
    
    reformatted my_bad_file.py
    
    All done! ✨ 🍰 ✨
    1 file reformatted.
    

    In this case, hooks with auto-fixing capabilities, like black or prettier, will modify the files directly. The developer simply needs to stage the modified files again (git add my_bad_file.py) and re-attempt the commit. This process ensures a clean commit history without messy "Fix lint" commits.

Conclusion: Why Should You Adopt pre-commit?

Adopting the pre-commit framework is more than just adding a tool; it's an effective way to improve your development culture.

  • Consistent Code Quality: Since every team member writes and checks code against the same rules, the overall code quality of the project is elevated.
  • Reduced Review Time: Code reviewers can focus on business logic instead of style nits or minor errors.
  • Automated Workflow: Developers can concentrate on development without worrying about repetitive tasks like linting or formatting.
  • Mistake Prevention: It enhances security by preventing sensitive information or debug code from being committed to the repository.

Initially, it may require a small effort to add the configuration and guide your team on how to use it. However, this small investment will pay off in the long run by maximizing team productivity and laying the foundation for more robust and maintainable code. Introduce pre-commit to your project today and experience the power of automated code quality management.

pre-commitフックで実現する、チーム開発の品質自動化

開発者であれば、「タイポ修正」や「リンター適用」といったコミットメッセージを一度は書いた経験があるでしょう。このような些細なミスは、コードレビューの過程で不要な時間を消費させ、チーム全体の生産性を低下させる原因となります。もし、これらのミスをコミットする前に自動的に修正できるとしたらどうでしょうか?まさにこの点で、Gitフック、特にpre-commitフックが強力な解決策として登場します。

この記事では、Gitフックの基本概念から始め、チーム単位のプロジェクトでコード品質を一貫して維持し、開発ワークフローを革新的に改善できるpre-commitフレームワークの設定と活用法を詳しく解説します。

Gitフックとは何か?

Gitフックとは、Gitの特定のイベント(例:コミット、プッシュ)が発生した際に自動的に実行されるスクリプトです。これにより、開発者は特定の条件が満たされない場合にコミットを中止させたり、コミットメッセージのフォーマットを強制したり、テストを自動実行したりするなど、様々な自動化タスクを実行できます。

Gitフックのスクリプトは、すべてのGitリポジトリの.git/hooks/ディレクトリ内に配置されています。git initで新しいリポジトリを作成すると、このディレクトリ内に様々なサンプル(.sample拡張子)が生成されているのを確認できます。

$ ls .git/hooks/
applypatch-msg.sample         pre-commit.sample           pre-rebase.sample
commit-msg.sample             pre-merge-commit.sample     pre-receive.sample
fsmonitor-watchman.sample     pre-push.sample             update.sample
post-update.sample            prepare-commit-msg.sample

これらのサンプルファイルの一つから.sampleという拡張子を削除し、実行権限を付与することで、そのフックが有効になります。例えば、pre-commit.sampleファイルの名前をpre-commitに変更して実行権限を与えると、git commitコマンドを実行する直前にそのスクリプトが実行されるようになります。

最も強力なフック:pre-commit

数あるフックの中でも、pre-commitは最も広く使われ、強力なフックの一つです。コミットが実際に作成される直前に実行されるため、コード品質に関連するほぼすべてのチェックをこの段階で実行できます。

  • コードスタイルチェック(リンティング):コードがチームのコーディング規約に従っているか確認します。
  • コードフォーマット:定められたルールに従ってコードスタイルを自動的に修正します。
  • 秘密鍵や機密情報の漏洩防止:コミットに誤って含まれたAPIキーやパスワードを検出します。
  • デバッグ用コードの混入防止:console.logdebuggerのようなコードがコミットされるのを防ぎます。
  • ユニットテストの実行:コミットしようとしているコードが既存のテストをパスするかを素早く確認します。

従来のGitフック方式の限界

.git/hooks/ディレクトリに直接シェルスクリプトを作成する方法はシンプルですが、チームプロジェクトではいくつかの致命的な欠点があります。

  1. バージョン管理ができない:.gitディレクトリはGitの追跡対象ではないため、フックスクリプトをチームメンバーと共有し、バージョンを管理することが非常に困難です。
  2. 設定が煩雑:新しいチームメンバーがプロジェクトに参加するたびに、手動でフックスクリプトを設定し、実行権限を付与する必要があります。
  3. 多様な言語環境への対応の難しさ:Python、JavaScript、Javaなど複数の言語を使用するプロジェクトでは、各言語に適したリンターやフォーマッターを設定・管理することが複雑になります。

これらの問題を解決するために登場したのが、pre-commitフレームワークです。

pre-commitフレームワークによるスマートな管理

pre-commitはPythonで作成されたGitフック管理フレームワークです。このフレームワークは、.pre-commit-config.yamlという設定ファイルを通じてフックを定義・管理します。このファイルはプロジェクトのルートに配置され、バージョン管理が可能なため、チームメンバー全員が同じフック設定を共有できます。

1. インストールと初期設定

まず、pre-commitをインストールします。Pythonのパッケージマネージャーであるpipを使用するのが一般的です。

# pipを使用してインストール
pip install pre-commit

# Homebrew (macOS)を使用してインストール
brew install pre-commit

インストールが完了したら、プロジェクトのルートディレクトリに.pre-commit-config.yamlファイルを作成します。このファイルに、使用するフックを定義します。

以下は基本的な設定ファイルの例です。

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0 # 常に最新の安定版を使用することを推奨します
    hooks:
    -   id: trailing-whitespace # ファイル末尾の空白を削除
    -   id: end-of-file-fixer # ファイルの末尾に改行を追加
    -   id: check-yaml # YAMLファイルの構文をチェック
    -   id: check-added-large-files # 大容量ファイルが追加されるのを防止

設定ファイルの作成が終わったら、次のコマンドを実行してGitフックを.git/hooks/pre-commitにインストールします。この作業は、プロジェクトを最初にクローンしたときに一度だけ実行すれば十分です。

pre-commit install

これで、git commitを試みると、pre-commitがステージングされたファイルに対して設定済みのフックを自動的に実行します。

2. 様々な言語向けのフックを追加する

pre-commitの真の強力さは、多様な言語やツールを容易に統合できる点にあります。例えば、Pythonプロジェクトではblack(フォーマッター)とruff(リンター)、JavaScriptプロジェクトではprettier(フォーマッター)とeslint(リンター)を追加できます。

Pythonプロジェクトの例 (black, ruff)

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
-   repo: https://github.com/psf/black
    rev: 24.4.2
    hooks:
    -   id: black
-   repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.4.4
    hooks:
    -   id: ruff
        args: [--fix] # 自動修正可能なエラーは修正する
    -   id: ruff-format

JavaScript/TypeScriptプロジェクトの例 (prettier, eslint)

# .pre-commit-config.yaml
repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.6.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
-   repo: https://github.com/prettier/prettier
    rev: 3.2.5
    hooks:
    -   id: prettier
        # 追加の引数を渡して特定のファイルタイプにのみ適用可能
        # types: [javascript, typescript, css, markdown]
-   repo: local # ローカルにインストールされたeslintを使用する場合
    hooks:
    -   id: eslint
        name: eslint
        entry: npx eslint --fix
        language: node
        types: [javascript, typescript]
        # 初回実行速度向上のため、常に実行するように設定
        always_run: true
        # ステージングされたファイルのみを引数として渡す
        pass_filenames: false

repo: localを使用すると、package.jsonに記載されたバージョンのツールを使用できるため、チームメンバー間のツールバージョンの不一致問題を解決できます。

3. 実際のワークフロー

これで全ての設定が完了しました。開発者がコードを修正した後にコミットを試みると、何が起こるでしょうか?

  1. 開発者がファイルを修正し、git add .コマンドでステージングします。
  2. git commit -m "機能追加"コマンドを実行します。
  3. pre-commitが自動的に起動し、.pre-commit-config.yamlに定義されたフックをステージングされたファイルに対して順次実行します。
  4. 成功シナリオ:すべてのフックが正常にパスすると、コミットが通常通り完了します。
    $ git commit -m "新機能の追加"
    Trim Trailing Whitespace........................................Passed
    Fix End of Files................................................Passed
    Check Yaml......................................................Passed
    black...........................................................Passed
    ruff............................................................Passed
    [feature/new-logic 1a2b3c4] 新機能の追加
     2 files changed, 15 insertions(+)
    
  5. 失敗シナリオ:一つ以上のフックが失敗した場合(例:リンティングエラーが発見された)、pre-commitは該当のエラーを出力し、コミットを中断します。
    $ git commit -m "バグ修正"
    Trim Trailing Whitespace........................................Passed
    Fix End of Files................................................Passed
    black...........................................................Failed
    - hook id: black
    - files were modified by this hook
    
    reformatted my_bad_file.py
    
    All done! ✨ 🍰 ✨
    1 file reformatted.
    

    この場合、blackprettierのように自動修正機能を持つフックはファイルを直接修正します。開発者は修正されたファイルを再度ステージング(git add my_bad_file.py)し、再びコミットを試みるだけです。このプロセスにより、「リンター修正」のような雑然としたコミットを残すことなく、常にクリーンなコードを維持できます。

結論:なぜpre-commitを導入すべきか?

pre-commitフレームワークを導入することは、単なるツールの追加を超え、開発文化そのものを改善する効果的な方法です。

  • 一貫したコード品質:すべてのチームメンバーが同じルールに従ってコードを記述・チェックするため、プロジェクト全体のコード品質が向上します。
  • レビュー時間の短縮:コードレビュアーはスタイルや些細なエラーではなく、ビジネスロジックにより集中できます。
  • 自動化されたワークフロー:開発者はリンティングやフォーマットといった反復的な作業を気にすることなく、開発に専念できます。
  • ミスの防止:機密情報やデバッグ用コードがリポジトリにコミットされるのを事前に防ぎ、セキュリティを強化します。

最初は設定を追加し、チームメンバーに使い方を案内する少しの努力が必要かもしれません。しかし、この小さな投資は、長期的にはチームの生産性を最大化し、より堅牢で保守しやすいコードを作る礎となるでしょう。今すぐあなたのプロジェクトにpre-commitを導入し、自動化されたコード品質管理の力を体験してみてください。

Flutter `const` 제대로 알고 쓰기: 성능 최적화의 첫걸음

Flutter 개발을 하다 보면 const 키워드를 마주치는 순간이 많습니다. 어떤 위젯 앞에는 붙어있고, 어떤 위젯 앞에는 없습니다. 안드로이드 스튜디오나 VS Code는 "이 위젯은 const로 만들 수 있어요!"라며 파란 줄을 긋기도 하죠. 많은 개발자들이 이 const를 단순히 '상수'를 의미하는 키워드로만 이해하고 넘어가거나, 린터(Linter)가 시키는 대로 기계적으로 추가하곤 합니다. 하지만 Flutter에서 const는 단순한 상수를 넘어, 앱의 성능을 극적으로 향상시킬 수 있는 매우 중요한 열쇠입니다.

이 글에서는 const가 왜 중요한지, final과는 무엇이 다른지, 그리고 const를 언제 어떻게 사용해야 앱의 퍼포먼스를 최대한으로 끌어올릴 수 있는지 구체적인 예시와 함께 깊이 있게 알아보겠습니다.

1. `const`와 `final`의 결정적 차이: 컴파일 타임 vs 런타임

const를 이해하기 위해선 final과의 차이점을 명확히 알아야 합니다. 둘 다 '한 번 할당되면 변경할 수 없는 변수'를 선언할 때 사용하지만, 값이 결정되는 시점이 완전히 다릅니다.

  • final (런타임 상수): 앱이 실행되는 동안(런타임) 값이 결정됩니다. 한 번 할당되면 바꿀 수 없지만, 그 값은 앱이 실행될 때 계산되거나 외부(API 등)로부터 받아올 수 있습니다.
  • const (컴파일 타임 상수): 코드가 컴파일되는 시점에 값이 결정되어야 합니다. 즉, 앱이 빌드될 때 이미 그 값이 무엇인지 명확하게 알고 있어야 합니다. 변수뿐만 아니라 객체(위젯 등)에도 사용할 수 있습니다.

예시를 통해 살펴보겠습니다.


// final: 앱 실행 시 현재 시간을 가져오므로 OK
final DateTime finalTime = DateTime.now();

// const: DateTime.now()는 실행 시점에 결정되므로 컴파일 에러 발생
// const DateTime constTime = DateTime.now(); // ERROR!

// const: 컴파일 시점에 값을 알 수 있으므로 OK
const String appName = 'My Awesome App';

이 차이점이 Flutter 위젯 트리에서 엄청난 성능 차이를 만들어냅니다.

2. `const`가 Flutter 성능을 향상시키는 두 가지 핵심 원리

const를 사용하는 것이 성능에 좋을까요? 이유는 크게 두 가지입니다: '메모리 재사용''불필요한 리빌드(Rebuild) 방지'입니다.

2.1. 메모리 효율성: 동일 객체 공유 (Canonical Instances)

const로 생성된 객체는 '정규 인스턴스(Canonical Instance)'가 됩니다. 이는 컴파일 시점에 값이 완전히 동일한 const 객체가 있다면, 앱 전체에서 단 하나의 인스턴스만 생성하고 모두가 그것을 공유한다는 의미입니다.

예를 들어, 앱의 여러 화면에서 동일한 간격을 주기 위해 const SizedBox(height: 20)를 100번 사용했다고 가정해 봅시다.


// const를 사용한 경우
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('첫 번째 아이템'),
      const SizedBox(height: 20), // A 인스턴스
      Text('두 번째 아이템'),
      const SizedBox(height: 20), // A 인스턴스를 재사용
      // ... 98번 더 반복
    ],
  );
}

이 경우, SizedBox(height: 20) 객체는 메모리에 단 하나만 생성되고, 100개의 모든 호출이 이 하나의 객체 주소를 참조합니다. 반면, const를 빼면 어떻게 될까요?


// const를 사용하지 않은 경우
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('첫 번째 아이템'),
      SizedBox(height: 20), // B 인스턴스 생성
      Text('두 번째 아이템'),
      SizedBox(height: 20), // C 인스턴스 생성 (B와 다름)
      // ... 98개의 새로운 인스턴스 생성
    ],
  );
}

const가 없으면 build 메소드가 호출될 때마다 새로운 SizedBox 객체가 100개 생성됩니다. 이는 불필요한 메모리 낭비와 가비지 컬렉터(GC)의 부담을 증가시켜 앱의 전반적인 성능 저하로 이어질 수 있습니다.

Dart의 identical() 함수를 사용하면 두 객체가 완전히 동일한 메모리 주소를 가리키는지 확인할 수 있습니다.


void checkIdentity() {
  const constBox1 = SizedBox(width: 10);
  const constBox2 = SizedBox(width: 10);
  print('const: ${identical(constBox1, constBox2)}'); // 출력: const: true

  final finalBox1 = SizedBox(width: 10);
  final finalBox2 = SizedBox(width: 10);
  print('final: ${identical(finalBox1, finalBox2)}'); // 출력: final: false
}

2.2. 렌더링 최적화: 불필요한 리빌드(Rebuild) 방지

이것이 const를 사용해야 하는 가장 중요한 이유입니다.

Flutter는 상태(State)가 변경될 때 setState()를 호출하여 위젯 트리를 다시 빌드(리빌드)합니다. 이때 Flutter 프레임워크는 이전 위젯 트리와 새로운 위젯 트리를 비교하여 변경된 부분만 화면에 다시 그립니다. 이 과정에서 위젯이 const로 선언되어 있다면, Flutter는 "이 위젯은 컴파일 타임 상수로, 절대 변하지 않는 위젯"이라는 사실을 알고 있습니다. 따라서 해당 위젯과 그 자식 위젯들에 대한 비교 작업을 완전히 건너뛰고, 리빌드 과정에서 제외시킵니다.

상태가 변경되는 카운터 앱을 예로 들어보겠습니다.

`const`를 사용하지 않은 나쁜 예


class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // 이 AppBar는 내용이 변하지 않음에도 불구하고 매번 리빌드됨
        title: Text('Bad Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // 이 부분도 변하지 않지만 매번 리빌드됨
            SizedBox(height: 50), 
            Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

위 코드에서 플로팅 버튼을 누를 때마다 _counter가 변경되고 setState()가 호출됩니다. 그러면 build 메소드 전체가 다시 실행됩니다. 이 과정에서 실제로 변경된 것은 Text('$_counter') 위젯뿐이지만, AppBar, SizedBox, Text('This is a static text.') 등 전혀 변경될 필요가 없는 위젯들까지 모두 새로 생성되고 비교 과정을 거치게 됩니다. 이는 매우 비효율적입니다.

`const`를 활용한 좋은 예


class CounterScreen extends StatefulWidget {
  // 위젯 자체도 const로 만들 수 있음
  const CounterScreen({Key? key}) : super(key: key);

  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // const 추가: 이 AppBar는 이제 리빌드 대상에서 제외됨
        title: const Text('Good Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // 이 텍스트는 변하지 않으므로 const
            const Text('You have pushed the button this many times:'),
            // 이 텍스트는 _counter 값에 따라 변하므로 const가 아님
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // const 추가: 이 SizedBox는 리빌드 대상에서 제외됨
            const SizedBox(height: 50),
            // const 추가: 이 텍스트는 리빌드 대상에서 제외됨
            const Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        // const 추가: Icon도 리빌드 대상에서 제외됨
        child: const Icon(Icons.add),
      ),
    );
  }
}

이제 버튼을 누르면 build 메소드는 여전히 호출되지만, Flutter는 const로 표시된 위젯들(AppBar, Text, SizedBox, Icon)을 보고 "아, 이것들은 바뀔 리가 없으니 그냥 건너뛰자!"라고 판단합니다. 결과적으로 실제로 변경이 필요한 Text('$_counter') 위젯만 업데이트하게 되어 렌더링 성능이 크게 향상됩니다.

3. `const` 활용 전략: 언제, 어디에 사용해야 할까?

성능 향상을 위해 const를 적극적으로 사용하는 습관을 들이는 것이 좋습니다. 다음은 const를 적용할 수 있는 주요 위치입니다.

3.1. 위젯 생성자 (Widget Constructors)

가장 흔하고 효과적인 사용처입니다. Text, SizedBox, Padding, Icon 등 내용이 고정된 위젯을 생성할 때는 항상 const를 붙이는 습관을 가지세요.


// GOOD
const Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

// BAD
Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

EdgeInsets.all(16.0) 역시 const로 만들 수 있으므로, Padding 위젯 전체가 const가 될 수 있습니다.

3.2. 나만의 `const` 생성자 만들기

재사용 가능성이 높은 나만의 위젯을 만들 때, const 생성자를 제공하는 것은 매우 중요합니다. 위젯의 모든 final 멤버 변수가 컴파일 타임 상수가 될 수 있다면 const 생성자를 만들 수 있습니다.


class MyCustomButton extends StatelessWidget {
  final String text;
  final Color color;

  // 생성자를 const로 선언
  const MyCustomButton({
    Key? key,
    required this.text,
    this.color = Colors.blue,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ... 위젯 빌드 로직
    return Container(
      color: color,
      child: Text(text),
    );
  }
}

// 사용할 때
// 이제 이 위젯도 const로 생성하여 리빌드를 방지할 수 있다.
const MyCustomButton(text: 'Click Me')

3.3. 변수 및 컬렉션 (Variables and Collections)

앱 전역에서 사용되는 상수 값들, 예를 들어 색상, 패딩 값, 특정 문자열 등은 const 변수로 선언하여 관리하는 것이 좋습니다.


// lib/constants.dart
import 'package:flutter/material.dart';

const Color kPrimaryColor = Color(0xFF6F35A5);
const double kDefaultPadding = 16.0;

const List<String> kWelcomeMessages = [
  'Hello',
  'Welcome',
  'Bienvenido',
];

이렇게 선언된 상수들은 컴파일 시점에 값이 고정되며, 메모리 효율성도 높일 수 있습니다.

3.4. 린터 규칙(Linter Rules) 활용하기

const를 빠뜨리지 않고 사용하도록 강제하는 것은 좋은 습관입니다. 프로젝트 루트의 analysis_options.yaml 파일에 다음 규칙들을 추가하면 IDE가 const를 추가하라고 알려주거나 자동으로 수정해 줍니다.


linter:
  rules:
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_const_constructors_in_immutables
  • prefer_const_constructors: const로 만들 수 있는 생성자 호출에 const를 붙이도록 권장합니다.
  • prefer_const_declarations: const로 선언할 수 있는 최상위 변수나 정적 변수에 const를 사용하도록 권장합니다.
  • prefer_const_constructors_in_immutables: @immutable 클래스에 const 생성자를 추가하도록 권장합니다.

결론: `const`는 선택이 아닌 필수

Flutter에서 const는 단순히 '상수'를 의미하는 키워드가 아닙니다. 메모리를 절약하고, CPU의 불필요한 연산을 줄여 앱의 렌더링 성능을 최적화하는 가장 간단하면서도 강력한 도구입니다. 특히 복잡한 UI를 가진 앱이나 저사양 기기에서도 부드러운 사용자 경험을 제공하기 위해서는 const의 적극적인 활용이 필수적입니다.

이제부터 코드를 작성할 때, "이 위젯은 내용이 바뀌나?"라고 스스로에게 질문해 보세요. 만약 대답이 "아니오"라면, 주저하지 말고 const를 붙여주세요. 이 작은 습관 하나가 모여 당신의 Flutter 앱을 훨씬 더 빠르고 효율적으로 만들어 줄 것입니다.

Unlocking Flutter Performance: The Power of `const`

When developing with Flutter, you frequently encounter the const keyword. It appears before some widgets but not others. Your IDE, whether it's Android Studio or VS Code, often highlights a widget with a blue line, suggesting, "This constructor can be 'const'." Many developers either dismiss const as just another way to declare a constant or mechanically add it whenever the linter insists. However, in Flutter, `const` is far more than a simple constant; it's a critical key to dramatically improving your app's performance.

This article will take a deep dive into why `const` is so important, how it fundamentally differs from `final`, and how to use it strategically to maximize your app's performance, complete with practical examples.

1. The Decisive Difference: `const` vs. `final` (Compile-time vs. Runtime)

To truly understand `const`, you must first grasp its difference from `final`. Both are used to declare variables that cannot be reassigned after their initial assignment, but the timing of when their value is determined is completely different.

  • final (Runtime Constant): The value is determined while the app is running (at runtime). Once assigned, it cannot be changed, but its initial value can be calculated at runtime or fetched from an external source like an API.
  • const (Compile-time Constant): The value must be determined when the code is compiled. This means the compiler must know the exact value when the app is being built. This applies not only to variables but also to objects, including widgets.

Let's look at an example:


// final: OK, because DateTime.now() is evaluated at runtime.
final DateTime finalTime = DateTime.now();

// const: COMPILE ERROR, because DateTime.now() can only be known at runtime.
// const DateTime constTime = DateTime.now(); // ERROR!

// const: OK, because the value is a literal known at compile-time.
const String appName = 'My Awesome App';

This distinction is what creates a massive performance difference within the Flutter widget tree.

2. Two Core Principles of How `const` Boosts Flutter Performance

So, why is using `const` so beneficial for performance? There are two main reasons: memory reuse and preventing unnecessary rebuilds.

2.1. Memory Efficiency: Sharing Canonical Instances

An object created with `const` becomes a "canonical instance." This means if multiple `const` objects with the exact same value exist in your code, the compiler creates only a single instance in memory, and all references point to that one shared object.

For instance, imagine you use `const SizedBox(height: 20)` 100 times across different screens to create consistent spacing.


// With const
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('First Item'),
      const SizedBox(height: 20), // Instance A
      Text('Second Item'),
      const SizedBox(height: 20), // Reuses Instance A
      // ... repeated 98 more times
    ],
  );
}

In this case, only one `SizedBox(height: 20)` object is ever created in memory. All 100 calls will reference the memory address of this single object. Now, what happens if we remove `const`?


// Without const
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('First Item'),
      SizedBox(height: 20), // Creates Instance B
      Text('Second Item'),
      SizedBox(height: 20), // Creates Instance C (different from B)
      // ... 98 more new instances are created
    ],
  );
}

Without `const`, every time the `build` method is called, 100 new `SizedBox` objects are instantiated. This leads to unnecessary memory consumption and increases the workload for the Garbage Collector (GC), which can degrade the overall performance of your app.

You can verify this behavior using Dart's `identical()` function, which checks if two references point to the exact same object in memory.


void checkIdentity() {
  const constBox1 = SizedBox(width: 10);
  const constBox2 = SizedBox(width: 10);
  print('const: ${identical(constBox1, constBox2)}'); // Prints: const: true

  final finalBox1 = SizedBox(width: 10);
  final finalBox2 = SizedBox(width: 10);
  print('final: ${identical(finalBox1, finalBox2)}'); // Prints: final: false
}

2.2. Render Optimization: Preventing Unnecessary Rebuilds

This is the most compelling reason to use `const`.

In Flutter, when a state changes (e.g., via a call to `setState()`), the framework rebuilds the widget tree. It then compares the new widget tree with the old one to determine what has changed and only repaints the modified parts of the screen. When a widget is declared as `const`, Flutter knows that it's a compile-time constant and can never change. Therefore, it can completely skip the evaluation and diffing process for that widget and its entire subtree, saving valuable CPU cycles.

Let's consider a classic counter app example.

Bad Example: Without `const`


class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // This AppBar is rebuilt on every increment, even though it never changes.
        title: Text('Bad Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // These are also rebuilt unnecessarily.
            SizedBox(height: 50), 
            Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

In the code above, every press of the floating action button triggers `setState()`, which rebuilds the entire `build` method. The only widget that actually changes is `Text('$_counter')`. However, the `AppBar`, the `SizedBox`, and the other `Text` widget are all needlessly recreated and re-evaluated, which is highly inefficient.

Good Example: With `const`


class CounterScreen extends StatefulWidget {
  // The widget itself can have a const constructor.
  const CounterScreen({Key? key}) : super(key: key);

  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // Add const: This AppBar is now excluded from rebuilds.
        title: const Text('Good Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // This text is static, so it should be const.
            const Text('You have pushed the button this many times:'),
            // This text depends on _counter, so it CANNOT be const.
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // Add const: This SizedBox is excluded from rebuilds.
            const SizedBox(height: 50),
            // Add const: This text is excluded from rebuilds.
            const Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        // Add const: The Icon is also excluded from rebuilds.
        child: const Icon(Icons.add),
      ),
    );
  }
}

Now, when the button is pressed, the `build` method is still called, but Flutter sees the `const` markers on the `AppBar`, `Text`, `SizedBox`, and `Icon`. It knows they are immutable and says, "Ah, these can't have changed, so I'll just skip them." As a result, only the `Text('$_counter')` widget is actually updated, leading to a significant improvement in rendering performance.

3. `const` Utilization Strategy: When and Where to Use It

You should make it a habit to use `const` proactively for performance gains. Here are the primary places to apply it.

3.1. Widget Constructors

This is the most common and effective use case. Always get into the habit of adding `const` when instantiating widgets with fixed content, such as `Text`, `SizedBox`, `Padding`, and `Icon`.


// GOOD
const Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

// BAD
Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

Note that `EdgeInsets.all(16.0)` can also be `const`, which allows the entire `Padding` widget to be `const`.

3.2. Creating Your Own `const` Constructors

When you build your own reusable widgets, providing a `const` constructor is crucial. You can create a `const` constructor if all of the widget's `final` member variables can be initialized with compile-time constants.


class MyCustomButton extends StatelessWidget {
  final String text;
  final Color color;

  // Declare the constructor as const
  const MyCustomButton({
    Key? key,
    required this.text,
    this.color = Colors.blue,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ... widget build logic
    return Container(
      color: color,
      child: Text(text),
    );
  }
}

// Usage:
// Now this widget can also be created as a const to prevent rebuilds.
const MyCustomButton(text: 'Click Me')

3.3. Variables and Collections

Global constant values used throughout your app, such as colors, padding values, or specific strings, are best managed as `const` variables.


// lib/constants.dart
import 'package:flutter/material.dart';

const Color kPrimaryColor = Color(0xFF6F35A5);
const double kDefaultPadding = 16.0;

const List<String> kWelcomeMessages = [
  'Hello',
  'Welcome',
  'Bienvenido',
];

Constants declared this way are fixed at compile-time and improve memory efficiency.

3.4. Leverage Linter Rules

Enforcing the use of `const` is a great practice. By adding the following rules to your `analysis_options.yaml` file at the project root, your IDE will prompt you to add `const` or even fix it for you automatically.


linter:
  rules:
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_const_constructors_in_immutables
  • prefer_const_constructors: Recommends using `const` for constructor calls that can be `const`.
  • prefer_const_declarations: Recommends using `const` for top-level or static variables that can be declared as `const`.
  • prefer_const_constructors_in_immutables: Recommends adding a `const` constructor to `@immutable` classes.

Conclusion: `const` is Not an Option, It's a Necessity

In Flutter, `const` is not just a keyword for defining constants. It is the simplest yet most powerful tool for optimizing your app's rendering performance by saving memory and reducing unnecessary CPU work. Proactive use of `const` is essential, especially for creating a smooth user experience in apps with complex UIs or on low-end devices.

From now on, when you write your code, ask yourself, "Does the content of this widget ever change?" If the answer is "no," don't hesitate to add `const`. This one small habit, compounded over time, will make your Flutter app significantly faster and more efficient.

Flutter `const`徹底解説:知らないと損するパフォーマンス最適化術

Flutterでの開発中、私たちは頻繁にconstキーワードに遭遇します。あるウィジェットの前には付いていて、他のウィジェットには付いていない。Android StudioやVS Codeは「このコンストラクタはconstにできます」と青い下線で教えてくれます。多くの開発者は、このconstを単なる「定数」を意味するキーワードとして軽く流したり、リンター(Linter)の指示通りに機械的に追加したりしがちです。しかし、Flutterにおいてconstは、単なる定数という概念を遥かに超え、アプリのパフォーマンスを劇的に向上させるための非常に重要な鍵なのです。

この記事では、constがなぜ重要なのか、finalとは何が違うのか、そしてconstをいつ、どのように使えばアプリのパフォーマンスを最大限に引き出せるのかを、具体的な例を交えて深く掘り下げていきます。

1. `const`と`final`の決定的な違い:コンパイル時 vs 実行時

constを理解するためには、まずfinalとの違いを明確に把握する必要があります。どちらも「一度代入されると変更できない変数」を宣言するために使われますが、値が決定されるタイミングが全く異なります。

  • final (実行時定数): アプリが実行されている間(ランタイム)に値が決定されます。一度代入されると変更できませんが、その値はアプリの実行時に計算されたり、外部(APIなど)から取得したりすることができます。
  • const (コンパイル時定数): コードがコンパイルされる時点(ビルド時)に値が決定されていなければなりません。つまり、アプリがビルドされる段階で、その値が何であるかが明確に分かっている必要があります。これは変数だけでなく、オブジェクト(ウィジェットなど)にも適用できます。

例を見てみましょう。


// final: アプリ実行時に現在時刻を取得するためOK
final DateTime finalTime = DateTime.now();

// const: DateTime.now()は実行時にしか決定できないため、コンパイルエラーになる
// const DateTime constTime = DateTime.now(); // エラー!

// const: コンパイル時に値が分かっているためOK
const String appName = 'My Awesome App';

この違いが、Flutterのウィジェットツリーにおいて絶大なパフォーマンスの差を生み出します。

2. `const`がFlutterのパフォーマンスを向上させる2つの核心的原理

なぜconstを使うとパフォーマンスが向上するのでしょうか?理由は大きく分けて2つあります。「メモリの再利用」「不要なリビルドの防止」です。

2.1. メモリ効率性:同一オブジェクトの共有(Canonical Instances)

constで生成されたオブジェクトは「正規インスタンス(Canonical Instance)」となります。これは、コンパイル時点で値が完全に同一のconstオブジェクトがコード内に複数存在する場合、アプリ全体でたった一つのインスタンスのみを生成し、すべてがそのインスタンスを共有するという意味です。

例えば、アプリの複数の画面で同じ間隔を設けるためにconst SizedBox(height: 20)を100回使ったとします。


// constを使用した場合
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('最初のアイテム'),
      const SizedBox(height: 20), // Aインスタンス
      Text('2番目のアイテム'),
      const SizedBox(height: 20), // Aインスタンスを再利用
      // ... さらに98回繰り返す
    ],
  );
}

この場合、SizedBox(height: 20)オブジェクトはメモリ上に一つだけ生成され、100回の呼び出しすべてがこの一つのオブジェクトのアドレスを参照します。一方、constを付けなかったらどうなるでしょうか?


// constを使用しない場合
Widget build(BuildContext context) {
  return Column(
    children: [
      Text('最初のアイテム'),
      SizedBox(height: 20), // Bインスタンスを生成
      Text('2番目のアイテム'),
      SizedBox(height: 20), // Cインスタンスを生成 (Bとは別物)
      // ... さらに98個の新しいインスタンスが生成される
    ],
  );
}

constがないと、buildメソッドが呼ばれるたびに、100個の新しいSizedBoxオブジェクトが生成されてしまいます。これは不要なメモリの浪費であり、ガベージコレクタ(GC)の負担を増やし、アプリ全体のパフォーマンス低下につながります。

Dartのidentical()関数を使えば、2つのオブジェクトが完全に同じメモリアドレスを指しているかを確認できます。


void checkIdentity() {
  const constBox1 = SizedBox(width: 10);
  const constBox2 = SizedBox(width: 10);
  print('const: ${identical(constBox1, constBox2)}'); // 出力: const: true

  final finalBox1 = SizedBox(width: 10);
  final finalBox2 = SizedBox(width: 10);
  print('final: ${identical(finalBox1, finalBox2)}'); // 出力: final: false
}

2.2. レンダリング最適化:不要なリビルド(Rebuild)の防止

これこそが、constを使うべき最も重要な理由です。

Flutterは、状態(State)が変更されたときにsetState()を呼び出し、ウィジェットツリーを再構築(リビルド)します。その際、Flutterフレームワークは古いウィジェットツリーと新しいウィジェットツリーを比較し、変更があった部分だけを画面に再描画します。このプロセスにおいて、ウィジェットがconstで宣言されていると、Flutterは「このウィジェットはコンパイル時定数であり、絶対に変化しない」という事実を認識します。その結果、該当ウィジェットとその子ウィジェットツリーに対する比較処理を完全にスキップし、リビルドの対象から除外するのです。

状態が変化するカウンターアプリを例に見てみましょう。

`const`を使わない悪い例


class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // このAppBarは内容が変わらないにも関わらず、毎回リビルドされる
        title: Text('Bad Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // この部分も変化しないが、毎回リビルドされる
            SizedBox(height: 50), 
            Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

上記のコードでは、フローティングボタンを押すたびに_counterが変わり、setState()が呼ばれます。するとbuildメソッド全体が再実行されます。実際に変更されたのはText('$_counter')ウィジェットだけですが、AppBarSizedBoxText('This is a static text.')といった、全く変更する必要のないウィジェットまで全てが新しく生成され、比較処理の対象となってしまいます。これは非常に非効率です。

`const`を活用した良い例


class CounterScreen extends StatefulWidget {
  // ウィジェット自体もconstにできる
  const CounterScreen({Key? key}) : super(key: key);

  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('CounterScreen build() called');
    return Scaffold(
      appBar: AppBar(
        // constを追加: このAppBarはリビルド対象から除外される
        title: const Text('Good Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            // このテキストは不変なのでconst
            const Text('You have pushed the button this many times:'),
            // このテキストは_counterに依存して変化するため、constにはできない
            Text('$_counter', style: Theme.of(context).textTheme.headline4),
            // constを追加: このSizedBoxはリビルド対象から除外される
            const SizedBox(height: 50),
            // constを追加: このテキストはリビルド対象から除外される
            const Text('This is a static text.'),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        // constを追加: Iconもリビルド対象から除外される
        child: const Icon(Icons.add),
      ),
    );
  }
}

こうすると、ボタンを押した際にbuildメソッドは呼ばれますが、Flutterはconstが付与されたウィジェット(AppBar, Text, SizedBox, Icon)を見て、「ああ、これらは変わるはずがないから、チェックは飛ばそう」と判断します。結果として、実際に変更が必要なText('$_counter')ウィジェットのみが更新され、レンダリングパフォーマンスが大幅に向上します。

3. `const`活用戦略:いつ、どこで使うべきか?

パフォーマンス向上のため、constを積極的に使う習慣を身につけることが推奨されます。以下はconstを適用できる主な箇所です。

3.1. ウィジェットのコンストラクタ

最も一般的で効果的な使い方です。Text, SizedBox, Padding, Iconなど、内容が固定されているウィジェットを生成する際は、常にconstを付ける習慣をつけましょう。


// GOOD
const Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

// BAD
Padding(
  padding: EdgeInsets.all(16.0),
  child: Text('Hello World'),
)

EdgeInsets.all(16.0)自体もconstにできるため、Paddingウィジェット全体をconstにできます。

3.2. 独自の`const`コンストラクタを作成する

再利用性の高い独自のウィジェットを作成する際、constコンストラクタを提供することは非常に重要です。ウィジェットの全てのfinalなメンバ変数がコンパイル時定数で初期化可能であれば、constコンストラクタを作成できます。


class MyCustomButton extends StatelessWidget {
  final String text;
  final Color color;

  // コンストラクタをconstで宣言
  const MyCustomButton({
    Key? key,
    required this.text,
    this.color = Colors.blue,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // ... ウィジェットのビルドロジック
    return Container(
      color: color,
      child: Text(text),
    );
  }
}

// 使用時
// これでこのウィジェットもconstで生成でき、リビルドを防止できる
const MyCustomButton(text: 'Click Me')

3.3. 変数とコレクション

アプリ全体で使われる定数値、例えば色、パディング値、特定の文字列などは、const変数として宣言して管理するのが良いでしょう。


// lib/constants.dart
import 'package:flutter/material.dart';

const Color kPrimaryColor = Color(0xFF6F35A5);
const double kDefaultPadding = 16.0;

const List<String> kWelcomeMessages = [
  'Hello',
  'Welcome',
  'Bienvenido',
];

このように宣言された定数は、コンパイル時に値が固定され、メモリ効率も高めることができます。

3.4. リンタールール(Linter Rules)の活用

constの付け忘れを防ぐために、ルールを強制するのは良い習慣です。プロジェクトルートのanalysis_options.yamlファイルに以下のルールを追加すると、IDEがconstの追加を促したり、自動で修正してくれたりします。


linter:
  rules:
    - prefer_const_constructors
    - prefer_const_declarations
    - prefer_const_constructors_in_immutables
  • prefer_const_constructors: constにできるコンストラクタ呼び出しにconstを付けることを推奨します。
  • prefer_const_declarations: constで宣言できるトップレベル変数や静的変数にconstを使うことを推奨します。
  • prefer_const_constructors_in_immutables: @immutableなクラスにconstコンストラクタを追加することを推奨します。

結論:`const`は選択肢ではなく、必須のテクニック

Flutterにおいて、constは単に「定数」を意味するキーワードではありません。メモリを節約し、CPUの不要な計算を減らすことで、アプリのレンダリングパフォーマンスを最適化するための、最もシンプルかつ強力なツールです。特に、複雑なUIを持つアプリや低スペックのデバイスでも滑らかなユーザー体験を提供するためには、constの積極的な活用が不可欠です。

これからはコードを書く際に、「このウィジェットの内容は変化するか?」と自問してみてください。もし答えが「いいえ」であれば、ためらわずにconstを付けましょう。この小さな習慣の積み重ねが、あなたのFlutterアプリをより速く、より効率的にしてくれるはずです。