매일 반복되는 창 정렬 작업, Lua 스크립트 한 줄로 끝내기
개발하다 보면 터미널, 브라우저, 에디터, Slack을 매번 같은 위치에 배치하는 작업을 하루에도 수십 번 반복하게 돼요. macOS의 Mission Control이나 Split View로는 한계가 있고, 매번 드래그하는 시간도 아까워요. Hammerspoon은 이런 반복 작업을 Lua 스크립트로 자동화할 수 있는 macOS 전용 도구예요.
-- init.lua 파일에 작성
-- VS Code는 왼쪽 2/3, 터미널은 오른쪽 1/3 배치
local function devLayout()
local vscode = hs.application.get("Code")
local terminal = hs.application.get("Terminal")
if vscode then
-- 화면의 왼쪽 66%에 배치
vscode:mainWindow():moveToUnit({x=0, y=0, w=0.66, h=1})
end
if terminal then
-- 화면의 오른쪽 34%에 배치
terminal:mainWindow():moveToUnit({x=0.66, y=0, w=0.34, h=1})
end
end
-- cmd+alt+d 누르면 개발 레이아웃 즉시 적용
hs.hotkey.bind({"cmd", "alt"}, "d", devLayout)
화면을 격자로 나누어 정밀하게 창 배치하기
hs.grid 모듈을 사용하면 화면을 원하는 크기의 격자로 나눌 수 있어요. 픽셀 단위로 계산할 필요 없이 격자 좌표만 지정하면 돼요.
-- 화면을 12x4 격자로 분할 (가로 12칸, 세로 4칸)
hs.grid.setGrid('12x4')
hs.grid.setMargins(hs.geometry.size(5, 5)) -- 창 사이 여백 5픽셀
-- 브라우저를 상단 왼쪽 6x2 영역에 배치
hs.hotkey.bind({"cmd", "shift"}, "1", function()
local chrome = hs.application.get("Google Chrome")
if chrome then
local win = chrome:focusedWindow()
hs.grid.set(win, '0,0 6x2') -- x,y 위치와 너비x높이
end
end)
-- Slack을 하단 오른쪽 4x2 영역에 배치
hs.hotkey.bind({"cmd", "shift"}, "2", function()
local slack = hs.application.get("Slack")
if slack then
local win = slack:focusedWindow()
hs.grid.set(win, '8,2 4x2') -- 오른쪽 하단 코너
end
end)
격자 시스템의 장점은 다양한 화면 크기에서도 비율이 유지된다는 거예요. 외부 모니터를 연결하거나 맥북 화면으로 전환해도 레이아웃이 자동으로 조정돼요.
작업 컨텍스트별로 자동 전환되는 레이아웃 구성
코딩할 때, 문서 작업할 때, 화상회의할 때마다 필요한 창 배치가 달라요. 각 상황에 맞는 레이아웃을 미리 정의해두면 단축키 하나로 작업 환경을 전환할 수 있어요.
-- 레이아웃 정의를 테이블로 관리
local layouts = {
-- 코딩 모드: 에디터 중심
coding = {
{"Code", nil, nil, {x=0, y=0, w=0.7, h=1}},
{"Terminal", nil, nil, {x=0.7, y=0, w=0.3, h=0.5}},
{"Safari", nil, nil, {x=0.7, y=0.5, w=0.3, h=0.5}}
},
-- 리서치 모드: 브라우저 중심
research = {
{"Safari", nil, nil, {x=0, y=0, w=0.5, h=1}},
{"Notion", nil, nil, {x=0.5, y=0, w=0.5, h=0.7}},
{"Notes", nil, nil, {x=0.5, y=0.7, w=0.5, h=0.3}}
},
-- 회의 모드: 화상회의 앱과 필기 앱
meeting = {
{"zoom.us", nil, nil, {x=0, y=0, w=0.75, h=1}},
{"Notion", nil, nil, {x=0.75, y=0, w=0.25, h=1}}
}
}
-- 레이아웃 적용 함수
local function applyLayout(layoutName)
local layout = layouts[layoutName]
if layout then
for _, appLayout in ipairs(layout) do
local appName = appLayout[1]
local app = hs.application.get(appName)
if app then
local win = app:mainWindow()
if win then
win:moveToUnit(appLayout[4])
end
end
end
-- 알림 표시
hs.alert.show(layoutName .. " 레이아웃 적용됨", 1)
end
end
-- 각 레이아웃별 단축키 설정
hs.hotkey.bind({"cmd", "alt"}, "c", function() applyLayout("coding") end)
hs.hotkey.bind({"cmd", "alt"}, "r", function() applyLayout("research") end)
hs.hotkey.bind({"cmd", "alt"}, "m", function() applyLayout("meeting") end)
특정 앱이 실행될 때 자동으로 창 배치하기
앱이 실행될 때마다 자동으로 정해진 위치로 이동시킬 수도 있어요. Spotify는 항상 오른쪽 하단, Finder는 중앙에 배치하는 식으로 규칙을 만들 수 있어요.
-- 앱별 기본 위치 설정
local appPositions = {
["Spotify"] = {x=0.6, y=0.5, w=0.4, h=0.5},
["Finder"] = {x=0.2, y=0.2, w=0.6, h=0.6},
["Calendar"] = {x=0.7, y=0, w=0.3, h=0.5},
["Messages"] = {x=0.7, y=0.5, w=0.3, h=0.5}
}
-- 앱 실행 감지 워처 설정
appWatcher = hs.application.watcher.new(function(appName, eventType, appObject)
-- 앱이 실행되거나 활성화될 때
if eventType == hs.application.watcher.launched or
eventType == hs.application.watcher.activated then
local position = appPositions[appName]
if position then
-- 0.5초 후에 창 배치 (앱이 완전히 로드되기를 기다림)
hs.timer.doAfter(0.5, function()
local app = hs.application.get(appName)
if app then
local win = app:mainWindow()
if win then
win:moveToUnit(position)
end
end
end)
end
end
end)
-- 워처 시작
appWatcher:start()
모니터 연결 상태에 따라 다른 레이아웃 적용
외부 모니터를 연결했을 때와 맥북 단독으로 사용할 때 창 배치를 다르게 설정할 수 있어요. 화면 변화를 감지해서 자동으로 레이아웃을 전환해요.
-- 모니터별 레이아웃 정의
local screenLayouts = {
-- 맥북 화면만 있을 때
laptop = function()
local screen = hs.screen.mainScreen()
hs.window.focusedWindow():moveToScreen(screen)
applyLayout("coding") -- 위에서 정의한 레이아웃 사용
end,
-- 외부 모니터 연결 시
external = function()
local screens = hs.screen.allScreens()
if #screens > 1 then
-- 메인 앱은 외부 모니터로
local external = screens[2] -- 보통 외부 모니터가 두 번째
local vscode = hs.application.get("Code")
if vscode then
vscode:mainWindow():moveToScreen(external)
vscode:mainWindow():maximize()
end
-- 보조 앱은 맥북 화면으로
local laptop = screens[1]
local slack = hs.application.get("Slack")
if slack then
slack:mainWindow():moveToScreen(laptop)
slack:mainWindow():moveToUnit({x=0, y=0, w=0.5, h=1})
end
end
end
}
-- 화면 변화 감지
screenWatcher = hs.screen.watcher.new(function()
local screens = hs.screen.allScreens()
if #screens == 1 then
screenLayouts.laptop()
else
screenLayouts.external()
end
end)
screenWatcher:start()
창 크기를 단계적으로 조절하는 스마트 리사이징
같은 단축키를 여러 번 누르면 창 크기가 단계적으로 변하도록 설정할 수 있어요. 예를 들어 cmd+alt+right를 누를 때마다 창이 50% → 66% → 75% 크기로 늘어나요.
-- 창 크기 사이클 정의
local sizeCycles = {
right = {
{x=0.5, y=0, w=0.5, h=1}, -- 오른쪽 절반
{x=0.33, y=0, w=0.67, h=1}, -- 오른쪽 2/3
{x=0.25, y=0, w=0.75, h=1} -- 오른쪽 3/4
},
left = {
{x=0, y=0, w=0.5, h=1}, -- 왼쪽 절반
{x=0, y=0, w=0.67, h=1}, -- 왼쪽 2/3
{x=0, y=0, w=0.75, h=1} -- 왼쪽 3/4
}
}
-- 현재 창 상태 저장
local windowStates = {}
-- 사이클 함수
local function cycleWindowSize(direction)
local win = hs.window.focusedWindow()
if not win then return end
local id = win:id()
local cycle = sizeCycles[direction]
-- 현재 상태 확인, 없으면 0으로 시작
if not windowStates[id] then
windowStates[id] = {}
end
if not windowStates[id][direction] then
windowStates[id][direction] = 0
end
-- 다음 크기로 이동
windowStates[id][direction] = (windowStates[id][direction] % #cycle) + 1
local newSize = cycle[windowStates[id][direction]]
win:moveToUnit(newSize)
end
-- 단축키 설정
hs.hotkey.bind({"cmd", "alt"}, "right", function()
cycleWindowSize("right")
end)
hs.hotkey.bind({"cmd", "alt"}, "left", function()
cycleWindowSize("left")
end)
창 포커스를 앱 이름으로 빠르게 전환
마우스를 사용하지 않고 키보드만으로 원하는 앱으로 포커스를 이동할 수 있어요. cmd+ctrl+s를 누르면 Slack으로, cmd+ctrl+c를 누르면 Chrome으로 바로 전환돼요.
-- 앱 전환 단축키 매핑
local appHotkeys = {
s = "Slack",
c = "Google Chrome",
v = "Code",
t = "Terminal",
n = "Notion",
f = "Finder",
m = "Messages"
}
-- 단축키 등록
for key, appName in pairs(appHotkeys) do
hs.hotkey.bind({"cmd", "ctrl"}, key, function()
local app = hs.application.get(appName)
if app then
app:activate() -- 앱 활성화
-- 앱이 숨겨져 있으면 보이게 함
if app:isHidden() then
app:unhide()
end
-- 최소화된 창이 있으면 복원
local win = app:mainWindow()
if win and win:isMinimized() then
win:unminimize()
end
else
-- 앱이 실행되지 않았으면 실행
hs.application.launchOrFocus(appName)
end
end)
end
현재 창 배치를 스냅샷으로 저장하고 복원하기
작업 중인 창 배치를 그대로 저장했다가 나중에 똑같이 복원할 수 있어요. 프로젝트별로 창 배치를 저장해두면 컨텍스트 스위칭이 훨씬 빨라져요.
-- 창 스냅샷 저장소
local snapshots = {}
-- 현재 창 배치 저장
local function saveSnapshot(name)
snapshots[name] = {}
-- 모든 보이는 창 정보 저장
local windows = hs.window.visibleWindows()
for _, win in ipairs(windows) do
local app = win:application()
if app then
local frame = win:frame()
table.insert(snapshots[name], {
app = app:name(),
title = win:title(),
frame = frame
})
end
end
-- 파일로 저장 (재시작 후에도 유지)
hs.settings.set("snapshots", snapshots)
hs.alert.show("'" .. name .. "' 스냅샷 저장됨", 2)
end
-- 스냅샷 복원
local function restoreSnapshot(name)
local snapshot = snapshots[name]
if not snapshot then
-- 저장된 설정에서 불러오기
snapshots = hs.settings.get("snapshots") or {}
snapshot = snapshots[name]
end
if snapshot then
for _, winInfo in ipairs(snapshot) do
local app = hs.application.get(winInfo.app)
if app then
-- 타이틀이 일치하는 창 찾기
for _, win in ipairs(app:allWindows()) do
if win:title() == winInfo.title then
win:setFrame(winInfo.frame)
break
end
end
end
end
hs.alert.show("'" .. name .. "' 스냅샷 복원됨", 2)
end
end
-- 단축키로 스냅샷 관리
hs.hotkey.bind({"cmd", "shift", "ctrl"}, "s", function()
saveSnapshot("project1")
end)
hs.hotkey.bind({"cmd", "shift", "ctrl"}, "r", function()
restoreSnapshot("project1")
end)
Hammerspoon의 진짜 강점은 이 모든 기능을 자유롭게 조합하고 확장할 수 있다는 거예요. WiFi 연결, 시간대, 배터리 상태 등 다양한 조건에 따라 창 배치를 자동화할 수 있어요. 한 번 설정해두면 창 정렬에 쓰던 시간을 완전히 없앨 수 있어요.