
Show HN: Home Maker: Makefile로 개발 도구 선언하기
요약
다양한 패키지 매니저로 설치된 개발 도구들을 하나의 Makefile로 관리하여 개발 환경을 선언적으로 재구축할 수 있게 돕는 'Home Maker' 시스템을 소개합니다.
핵심 포인트
- 여러 패키지 매니저(APT, Cargo, UV 등)를 하나의 Makefile로 통합 관리
- 개발 환경 재설치 및 업그레이드 시 발생하는 기억력 문제 해결
- 추가 도구 없이 익숙한 텍스트 파일(Makefile)로 환경 선언
- 용도별 .mk 파일 분리를 통한 모듈화된 도구 관리
당신의 노트북에는 cargo install을 통해 설치된 ripgrep이 있습니다.
. ruff 역시 uv tool install을 통해 설치되어 있습니다.
. golangci-lint는 go install로 설치되었습니다.
. bash-language-server는 npm i -g로 설치되었습니다.
. Neovim은 tarball 다운로드로 설치되었습니다. Kitty는 curl 스크립트로 설치되었습니다.
6개월 후, 새로운 기기를 구매하거나 단순히 업그레이드 또는 재설치를 하고 싶어집니다. 도대체 무엇이 설치되어 있었나요? 각각 어떻게 설치했나요? 버전은 무엇이었나요? 행운을 빕니다.
이것은 그러한 질문들에 답을 주는 작은 시스템입니다. 용도별로 그룹화되어, 단 하나의 명령어로 무엇이든 설치할 수 있도록 당신이 신경 쓰는 모든 도구를 선언하는 단일 Makefile입니다.
참고: 이 도구를 바로 사용하고 싶다면, github.com/santhoshtr/hm으로 가서 패키지를 추가하기 시작하세요. 이 포스트의 나머지 부분은 이것이 어떻게 작동하는지 설명합니다.
문제점 (The Problem)
개발자의 머신에는 다섯 개 이상의 패키지 매니저(package managers)로부터 온 도구들이 쌓입니다. 각 매니저는 자신만의 구문을 가지고 있습니다:
sudo apt-get install -y ripgrep
cargo install eza
uv tool install ruff
...
당신은 설치하는 날에는 이것들을 기억합니다. 하지만 3개월 후에는 아무것도 기억나지 않습니다. 다음 노트북이 오거나, 일부 패키지를 업그레이드 또는 재설치하고 싶을 때, 당신은 이전에 무엇을 사용했는지, 어떻게 가져오는지 다시 찾아내는 데 하루를 소비합니다.
해결책은 또 다른 도구가 아닙니다. 그것은 당신이 이미 이해하고 있는 텍스트 파일입니다. 터미널에서 설치 명령과 스크립트를 실행하는 대신, 미래를 위해 그것을 기록하는 것입니다.
하나의 Makefile (One Makefile)
디렉토리를 생성합니다. 그 안에 단 하나의 Makefile과
dev/ 디렉토리 아래의 몇 가지 .mk 파일들을 둡니다:
hm/
├── Makefile
├── dev/
...
각 .mk 파일은 단순한 += 추가 연산자를 사용하여 하나의 매니저에 대한 패키지들을 선언합니다:
# dev/cli.mk
APT += ripgrep jq bat fzf htop tmux
CARGO += eza zoxide fd
...
# dev/python.mk
UV += ruff black isort pyright
# dev/go.mk
GO += gopls golangci-lint
PKG_gopls := golang.org/x/tools/gopls
...
이것이 패키지 선언 시스템의 전부입니다. 매니저당 하나씩, 총 다섯 개의 변수(APT, CARGO, UV, GO, NPM)가 있습니다. 각 .mk
파일은 필요한 변수에 추가됩니다. Makefile은 이들을 모두 포함합니다.
Makefile의 작동 방식
중앙 Makefile은 세 가지 작업을 수행합니다: .mk 파일을 포함하고, 타겟 생성기 (target generators)를 정의하며, 리스트를 타겟으로 확장합니다.
모든 파일 포함:
include dev/cli.mk
include dev/python.mk
# ... 등등
설치 명령 정의:
APT_INSTALL := sudo apt-get install -y
CARGO_INSTALL := cargo install
UV_INSTALL := uv tool install
...
name@version 구문을 분리하고 PKG_ 오버라이드 (overrides)를 해결하기 위한 헬퍼 함수 (helper functions) 정의:
pkg-name = $(firstword $(subst @, ,$(1)))
pkg-version = $(word 2,$(subst @, ,$(1)))
pkg-pkgname = $(or $(PKG_$(call pkg-name,$(1))),$(call pkg-name,$(1)))
매니저당 생성기 매크로 (generator macro) 정의. 다음은 gen-apt입니다:
define gen-apt
.PHONY: $(call pkg-name,$(1))
$(call pkg-name,$(1)):
...
나머지 네 개(gen-cargo, gen-uv, gen-go, gen-npm)도 동일한 패턴을 따릅니다.
모든 리스트를 타겟으로 확장:
$(foreach p,$(APT),$(eval $(call gen-apt,$(p))))
$(foreach p,$(CARGO),$(eval $(call gen-cargo,$(p))))
$(foreach p,$(UV),$(eval $(call gen-uv,$(p))))
...
이것이 전체 메커니즘입니다. 생성된 파일도, YAML도, DSL도 없습니다. make는 foreach를 평가하고, 각 항목을 확장하며, .PHONY 타겟을 생성합니다. make ripgrep은 sudo apt-get install -y ripgrep을 실행합니다. make ruff는 uv tool install ruff를 실행합니다. 모든 타겟은 어떤 .mk 파일의 리스트에 추가되었기 때문에 나타납니다.
모든 실행은 도구가 이미 설치되어 있는지 확인하지 않고 패키지 매니저를 호출하며, 이는 의도된 것입니다. 이 시스템은 해당 로직을 중복해서 구현하지 않습니다. apt-get은 이미 존재하면 건너뛰고, cargo install은 더 새로운 버전이 있으면 업그레이드하며, uv tool install은 재설치합니다. Makefile은 선언 (declaration)이며, 무엇을 할지는 패키지 매니저가 결정합니다.
패키지를 추가하는 세 가지 패턴
패턴 1: 패키지 매니저가 보유하고 있으며 이름이 일치함. 리스트에 추가합니다:
# dev/cli.mk
APT += htop
완료되었습니다. make htop을 실행하면 즉시 작동합니다.
패턴 2: 버전 고정 (pin a version). name@version 형식을 사용합니다:
gen-uv 매크로는 이를 uv tool install black==24.2.0으로 변환합니다. gen-go 매크로는 @v1.55.0을 사용합니다. gen-npm 및 gen-cargo 매크로는 각자의 문법을 처리합니다. 버전은 선택 사항입니다. 버전을 생략하면 최신 버전을 가져옵니다.
패턴 3: 타겟 이름이 패키지 이름과 다름. PKG_를 사용합니다:
CARGO += fd
PKG_fd := fd-find
make fd를 실행하면 cargo install fd-find가 실행됩니다. 타겟 이름은 사용자가 입력하는 것이고, PKG_ 값은 패키지 매니저가 인식하는 값입니다.
커스텀 설치 스크립트 (Custom Install Scripts)
일부 도구는 패키지 매니저에 존재하지 않습니다. 이들은 GitHub 릴리스 타르볼 (tarballs), curl 스크립트, 또는 로컬 빌드를 통해 제공됩니다. 직접 작성한 타겟을 작성하세요:
.PHONY: uv
uv:
@echo "installing/upgrading $@..."
...
이러한 타겟들은 생성된 타겟들과 정확히 동일하게 작동합니다. make 탭 완성 (tab-completion) 및 대화형 설치기 (interactive installer)에 나타납니다. 차이점은 설치 명령어를 직접 작성한다는 것입니다.
그룹 타겟 (Group Targets)
개별 패키지들은 목적에 따라 그룹화됩니다:
.PHONY: cli
cli: ripgrep jq bat fzf htop tmux eza zoxide fd
.PHONY: python
...
메타 타겟 (Meta-targets)은 이들을 하나로 묶습니다:
.PHONY: dev
dev: cli python node go rust lsp
.PHONY: all
...
make all은 모든 것을 설치합니다. make cli는 CLI 도구들만 설치합니다. make ripgrep은 단일 패키지를 설치합니다. 동일한 시스템 내에서 세 단계의 세분성 (granularity)을 제공합니다.
대화형 설치기 (The Interactive Installer)
Makefile은 make <target>을 제공합니다. 이는 잘 작동합니다. 하지만 10개의 파일에 걸쳐 50개의 패키지가 있다면, 브라우징과 검색이 필요할 것입니다.
hm.sh는 단 한 가지 일을 수행하는 30줄짜리 쉘 스크립트입니다: Makefile의 모든 타겟을 찾아내어 fzf로 보여줍니다.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
...
핵심이 되는 라인은 make -pn입니다.
그것은 Make의 “dry run, print database” (실행 없이 데이터베이스 출력) 모드입니다. 이 모드는 아무것도 실행하지 않고 모든 타겟(target), 의존성(dependencies), 그리고 레시피(recipe)를 쏟아냅니다. 스크립트는 해당 출력을 파싱하여 그룹 및 메타 타겟을 필터링한 후, 남은 항목들을 fzf에 전달합니다.
:
selected=$(packages | fzf \
--multi \
--prompt "install > " \
...
미리보기 창(preview pane)은 make -n <target> (dry run)을 실행하므로, 실행을 확정하기 전에 정확히 어떤 명령어가 실행될지 확인할 수 있습니다. Tab 키로 여러 타겟을 선택할 수 있고, Enter 키로 실행합니다. 루프는 Esc를 누를 때까지 계속됩니다.
핵심적인 속성은 스크립트에 하드코딩된 내용이 전혀 없다는 점입니다. 스크립트는 Makefile 데이터베이스를 실시간으로 읽습니다. 어떤 .mk 파일에든 패키지를 추가하면, 다음에 hm을 실행할 때 fzf에 즉시 나타납니다. 업데이트를 위해 다른 곳을 찾아볼 필요가 없습니다.
PATH에 심볼릭 링크(Symlink)를 생성하세요:
ln -s "$(pwd)/hm.sh" ~/bin/hm
이제 어디서든 hm을 사용할 수 있습니다.
정리하기 (Cleaning Up)
.PHONY: clean
clean:
@cargo cache -a 2>/dev/null || true
...
명령어 하나로 모든 패키지 매니저의 캐시를 제거합니다. || true 가드(guards)는 특정 매니저가 설치되어 있지 않더라도 프로세스가 중단되지 않도록 보장합니다.
왜 Nix나 Ansible이 아닌가?
Nix는 원자적 롤백(atomic rollbacks)이 가능한 재현 가능하고 격리된(hermetic) 환경이라는 실제적인 문제를 해결합니다. 하지만 Nix를 사용하려면 함수형 언어(functional language)를 배워야 하고, 병렬적인 패키지 생태계를 받아들여야 하며, 문제가 발생했을 때 Nix 내부 구조를 디버깅해야 합니다. 학습 곡선(learning curve)이 몇 주에 달합니다. 개인용 개발 머신에서 단순히 “ripgrep를 설치하기 위해” 사용하기에는 너무 과한 메커니즘입니다.
Ansible은 플릿 관리(fleet management, 대규모 서버 군 관리)를 위해 설계되었습니다. 인벤토리(inventory), YAML 플레이북(playbooks), Python 런타임, 그리고 원격 호스트에 대한 SSH 연결을 요구합니다. 노트북 한 대는 플릿이 아닙니다.
**Docker/컨테이너(containers)**는 격리(isolation) 문제를 해결하는 것이지, 도구 설치 문제를 해결하는 것이 아닙니다. 에디터, 터미널, 그리고 CLI 도구들을 컨테이너 내부에서 실행하고 싶지는 않을 것입니다.
이 시스템은 make와 bash를 사용합니다.
Linux를 사용하는 모든 개발자는 이미 두 가지 모두를 알고 있습니다. 데몬 (daemon)도, 숨겨진 상태 (hidden state)도, 종속 (lock-in)도 없습니다. 무언가 실패하면 쉘 명령어를 읽으면 됩니다. 도구를 추가하고 싶다면 한 줄만 추가하면 됩니다. 새로운 머신으로 옮기고 싶다면, 저장소를 클론 (clone)하고 make all을 실행하면 됩니다.
트레이드오프 (tradeoffs)는 솔직합니다. 설치가 완전히 격리 (hermetic)되지는 않으며, 롤백 (rollback) 기능이 없고, 재현성 (reproducibility)은 업스트림 (upstream)이 안정적으로 유지되는지에 달려 있습니다. 개인 개발용 머신을 위해서는, 이해하는 데 5분이 걸리고 지속적인 유지보수가 전혀 필요 없는 시스템과 맞바꿀 수 있는 수용 가능한 비용입니다.
시작하기
저장소를 클론 (clone)하고, Makefile을 읽은 뒤, 자신만의 패키지를 추가하기 시작하세요:
git clone https://github.com/santhoshtr/hm.git
cd hm
.mk 파일을 커스텀하여 도구들을 선언하세요. dev/cli.mk를 열고 필요한 목록에 다음을 추가하세요:
APT += wget
그 다음 사용하세요:
# 패키지 하나 설치
make wget
# 그룹 하나 설치
...

마무리하며
이 시스템은 도구를 설치합니다. 그것이 이 시스템이 하는 전부입니다. 도구의 설정 — dotfiles, 에디터 설정, 쉘 프로필 (shell profiles) — 을 관리하지는 않습니다. 이를 위해 저는 개인 git 저장소와 함께 GNU Stow를 사용합니다. Stow는 설정 파일들을 적절한 위치에 심볼릭 링크 (symlinks)로 연결합니다. 그것은 별개의 관심사이며, 두 시스템을 분리해 두는 것이 각각을 단순하게 유지하는 방법입니다.
당신은 시스템의 도구들을 관리하기 위해 무엇을 사용하고 계신가요?
AI 자동 생성 콘텐츠
본 콘텐츠는 HN OpenAI Codex의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기