「담당자가 작성한 PowerShell 스크립트는 돌아가고 있지만, 그 사람만 손댈 수 있다」「서버명이나 경로가 코드 곳곳에 직접 적혀 있어서, 환경이 바뀔 때마다 본체를 고쳐 쓰고 있다」「인수를 잘못 넘겨도 조용히 동작해 버려서 나중에야 알아챘다」── 운영 자동화 상담에서 스크립트 그 자체보다도 「스크립트를 넘기는 방법·키우는 방법」이 문제가 되는 사례가 매우 많습니다.
개인이 직접 돌리기만 한다면 변수를 직접 적어 넣은 「작동하는 스크립트」로도 문제없습니다. 하지만 작업 스케줄러에 올리거나, 동료에게 넘기거나, 여러 대의 서버에서 돌려 쓰게 되는 순간, 인수 설계와 공통 처리의 정리가 품질을 좌우합니다. 다행히 PowerShell에는 이 「남에게 넘길 수 있는 스크립트」로 가기 위한 도구가 언어 기능으로 처음부터 마련되어 있습니다. param 블록, 검증 속성, 주석 기반 도움말, 그리고 모듈입니다.
이 글에서는 중소기업의 정보시스템·운영 담당자가 이미 가지고 있는 「작동하는 스크립트」를 출발점으로 삼아, 인수 설계 → 입력 검증 → 도움말과 -WhatIf 대응 → .psm1 모듈화 → 사내 공유라는 순서로 단계적으로 품질을 끌어올리는 실무 절차를 정리합니다. PowerShell 7.x를 기준으로 하되, Windows PowerShell 5.1만 사용할 수 있는 현장에서의 주의점도 그때그때 보충합니다.
1. 먼저 결론
- 인수는 param 블록으로 선언하고, [CmdletBinding()]을 붙여 「고급 함수」로 만드는 것이 출발점입니다. 공통 매개변수(-Verbose, -ErrorAction 등)가 자동으로 붙고, 정의되지 않은 매개변수를 넘기면 바인드 오류가 되므로, 오타가 조용히 무시되는 사고를 막을 수 있습니다.1
- 필수 인수는 [Parameter(Mandatory)]로 선언하고, 형식을 반드시 붙입니다. 필수 지정을 빠뜨린 호출은 실행 전에 멈추고, 형식이 맞지 않는 값도 실행 전에 걸러집니다.2
- 형식 체크는 if문이 아니라 검증 속성(ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty)으로 몰아줍니다. 검증에 실패하면 함수가 호출되지 않아, 「도중까지 실행되다가 망가지는」 상황을 구조적으로 없앨 수 있습니다. 오류는 입구에서 빨리 내는 것이 원칙입니다.2
- 켜기/끄기 인수는 [switch]로 선언합니다. $true나 $false를 문자열로 받는 자기류의 플래그 인수는 호출하는 쪽의 사고로 이어집니다.2
- 주석 기반 도움말(.SYNOPSIS/.EXAMPLE)을 작성하면, 자체 제작한 명령에도 Get-Help가 작동합니다. 「사용법은 코드를 읽어 달라」에서 벗어나는 것이, 남에게 넘기기 위한 최소 조건입니다.3
- 변경계 함수는 SupportsShouldProcess를 선언해 -WhatIf/-Confirm에 대응시킵니다. 호출하는 쪽이 실행 전에 영향 범위를 확인할 수 있다는 것은, 운영 스크립트의 안전장치로서 비용 대비 효과가 가장 큽니다.4
- 여러 스크립트에서 돌려 쓰는 함수는 .psm1 모듈로 분리해, $env:PSModulePath 아래에 둡니다. 두는 위치만 올바르면 Import-Module 없이도 자동으로 로드됩니다. psd1 매니페스트는 「배포하는 단계」에서 추가하면 충분합니다.5678
- 모듈과 스크립트는 Git으로 버전 관리하고, 공유 폴더 배포에서는 실행 정책과의 관계에 주의합니다. UNC 경로 상의 스크립트는 RemoteSigned에서 실행을 거부당하는 경우가 있습니다.9
2. param 블록과 [CmdletBinding()] ── 「고급 함수」로 가는 입구
먼저, 현장에서 자주 보는 「작동하는 스크립트」의 전형은 이렇습니다.
# 자주 보는 예: 변수를 직접 적어 넣음. 환경이 바뀔 때마다 본체를 고쳐 써야 한다
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }
이것을 param 블록과 [CmdletBinding()]으로 다시 작성합니다.
[CmdletBinding()]
param(
# 필수. 지정을 빠뜨린 호출은 실행 전에 멈춘다
# (주: [string]은 의도의 명시일 뿐 거부는 아니다. 숫자 등은 문자열로 자동 변환되어
# 넘어오므로, 엄격하게 걸러내고 싶은 조건은 뒤에 나오는 검증 속성으로 작성한다)
[Parameter(Mandatory)]
[string]$LogDir,
# 생략 가능한 인수에는 업무상 안전한 기본값을 둔다
[int]$Days = 90
)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }
[CmdletBinding()]은 「이 함수(스크립트)를 컴파일된 cmdlet과 같은 방식으로 동작시킨다」는 선언으로, 이를 붙인 함수는 고급 함수(advanced function)가 됩니다. 효과는 세 가지입니다.1
- 공통 매개변수가 자동으로 붙는다. -Verbose, -Debug, -ErrorAction, -ErrorVariable 등을 직접 구현하지 않아도 호출하는 쪽이 사용할 수 있게 됩니다. 스크립트 안의 Write-Verbose가 -Verbose를 지정했을 때만 표시된다는 표준적인 동작을 그대로 얻을 수 있습니다.
- 인수 실수가 실행 전에 멈춘다. 고급 함수에서는 정의되지 않은 매개변수 이름이나, 대응하는 위치 매개변수가 없는 여분의 인수를 넘기면 매개변수 바인드가 실패합니다.
-Dyas 30처럼 오타가 조용히 무시되고 기본값으로 동작해 버리는 사고가 없어집니다.1 - $PSCmdlet을 사용할 수 있다. 뒤에서 다룰 ShouldProcess 등, cmdlet을 위한 기능으로 가는 입구입니다.10
Mandatory를 붙인 인수가 생략되면, PowerShell은 실행 전에 입력을 요구합니다. 「필수인데 넘기는 것을 깜빡한 채 기본값으로 동작했다」를 막는, 첫 번째 안전장치입니다.2 한 가지 주의할 점은, 이 「입력을 요구하는」 동작이 무인 실행과 궁합이 나쁘다는 것입니다. 작업 스케줄러에서 실행할 때 필수 인수가 빠져 있으면, 아무도 대답할 수 없는 프롬프트에서 작업이 멈춘 채로 남을 수 있습니다. 무인 실행에서는 -NonInteractive를 붙여 실행해서, 프롬프트 대신 즉시 오류로 실패하게 하는 것이 정석입니다.
「어디에 적어야 하는지」가 헷갈리기 쉬우므로, 작업 스케줄러의 「동작」 탭 기입 예시도 들어 둡니다(PowerShell 7로 실행하는 경우).
| 항목 | 기입 예시 |
|---|---|
| 프로그램/스크립트 | C:\Program Files\PowerShell\7\pwsh.exe |
| 인수 추가(옵션) | -NoProfile -NonInteractive -File "C:\Scripts\Remove-OldAppLog.ps1" -LogDir "D:\Logs\AppA" -Days 90 |
| 시작 위치(옵션) | C:\Scripts |
Windows PowerShell 5.1로 실행한다면 프로그램 항목은 powershell.exe입니다. 핵심은 세 가지입니다. -NoProfile로 프로필 로드에 따른 환경 차이와 시작 지연을 없애는 것, -NonInteractive로 앞서 말한 프롬프트 대기를 막는 것, 그리고 스크립트 경로도 인수 값도, 공백이 들어갈 수 있다면 따옴표로 감싸는 것입니다. -File 뒤에 적은 것은 스크립트의 인수로 전달되므로, 스크립트 자체의 매개변수는 -File보다 뒤에 나열합니다. 「시작 위치(옵션)」를 비워 두면 작업 디렉터리가 기본 위치가 되므로, 상대 경로를 사용하는 스크립트에서는 반드시 지정하십시오.
참고로 함수 이름을 붙여서 공개할 때는 Verb-Noun 형식으로 하고, 동사는 Get-Verb로 확인할 수 있는 승인된 동사 중에서 선택합니다. 승인되지 않은 동사라도 동작은 하지만, 모듈을 임포트할 때 경고가 표시됩니다.11
3. 입력 검증은 입구에서 ── 검증 속성으로 「오류를 빨리」
인수의 형식 체크를 함수 본체의 if문으로 작성하면, 체크 누락이나 「체크보다 먼저 부작용이 실행되는」 버그가 섞여 들어가기 쉽습니다. PowerShell에서는 검증 속성으로 매개변수 선언에 검증을 함께 담을 수 있습니다. 검증은 함수가 호출되기 전에 이루어지며, 실패하면 본체는 한 줄도 실행되지 않습니다.2
[CmdletBinding()]
param(
# 존재하는 폴더만 허용한다. $_ 가 검증 대상 값
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
# 보관 일수는 1~3650일로 제한. 0이나 음수로 「전체 파일 대상」이 되는 사고를 막는다
[ValidateRange(1, 3650)]
[int]$Days = 90,
# 선택지를 고정. 탭 완성도 작동하게 된다
[ValidateSet('Zip', 'Move', 'ReportOnly')]
[string]$Mode = 'ReportOnly',
# 빈 문자열·$null을 거부. 문자열 인수의 기본 장비
[ValidateNotNullOrEmpty()]
[string]$ReportName = 'log-report',
# 켜기/끄기는 switch. 지정하면 $true, 생략하면 $false
[switch]$IncludeSubfolders
)
구분해서 사용하는 기준을 정리합니다.
| 속성 | 용도 | 현장에서의 전형적인 예 |
|---|---|---|
| ValidateSet | 값을 고정된 선택지로 제한하고, 탭 완성을 작동시킨다2 | 동작 모드, 환경 이름(Dev/Test/Prod) |
| ValidateRange | 숫자·날짜의 범위 제한2 | 보관 일수, 재시도 횟수, 포트 번호 |
| ValidateScript | 임의의 스크립트로 검증. $false나 예외로 실패2 | 경로 존재 확인, 날짜의 전후 관계 |
| ValidateNotNullOrEmpty | $null·빈 문자열·빈 컬렉션을 거부2 | 거의 모든 문자열 인수 |
| ValidatePattern | 정규식으로 형식 체크2 | 전표 번호, 호스트명 명명 규칙 |
두 가지를 보충합니다. 첫째, 검증 속성은 선언 순서에 주의가 필요합니다. 형식보다 뒤에 검증 속성을 쓰면, 형식 변환 전의 값이 검증되어 예기치 않은 실패를 부르는 경우가 있으므로, 속성 → 형식 → 변수명 순서로 작성하는 것이 공식 문서가 권장하는 모범 사례입니다.2 둘째, ValidateScript의 ErrorMessage 인수(독자적인 오류 메시지)는 PowerShell 6 이후의 기능으로, Windows PowerShell 5.1에서는 사용할 수 없습니다.2 5.1이 섞여 있는 환경에서는 검증 스크립트 안에서 throw로 직접 메시지를 내거나, 기본 메시지 그대로 두는 편이 무난합니다.
ValidateScript로 공들인 검증을 쓰기 시작했다면, 그것은 테스트를 작성해야 한다는 신호이기도 합니다. 검증 로직 자체의 동작 확인은 「Pester로 하는 PowerShell 테스트 정비」에서 정리한 형태에 실으면 쉽게 망가지지 않게 됩니다.
4. 파이프라인 입력의 기본 ── ValueFromPipeline과 process 블록
자체 제작한 함수도 Get-Content servers.txt | Test-AppServer처럼 파이프라인으로 사용할 수 있으면, PowerShell의 표준 명령과 같은 감각으로 조합할 수 있습니다. 필요한 것은 ValueFromPipeline 선언과 process 블록, 이 두 가지입니다.2
function Test-AppServer {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
begin { $results = @() } # 파이프라인 처리 전에 한 번만 실행
process {
# 파이프라인에서 흘러오는 요소마다 실행된다
foreach ($name in $ComputerName) {
$results += [pscustomobject]@{
ComputerName = $name
# -ComputerName은 5.1과 7 양쪽에서 사용할 수 있다(7에서 추가된 -TargetName은 5.1에 없다)
Reachable = Test-Connection -ComputerName $name -Count 1 -Quiet
}
}
}
end { $results } # 마지막에 한 번만 실행
}
짚고 넘어가야 할 점은 하나뿐입니다. 파이프라인 입력을 받는다면 process 블록에 처리를 작성할 것. process 블록이 없으면, 파이프라인으로 여러 값을 흘려보내도 마지막 1건만 처리되는 전형적인 버그가 됩니다.10 begin과 end는 생략 가능하므로, 헷갈리면 「process에 본체, 집계가 필요하면 begin/end」라고 기억해 두면 실무에서는 충분합니다.
함정이 잘 와닿지 않는 부분이므로, NG 예시와 나란히 놓아 둡니다.
# NG 예시: process 블록이 없다. 파이프라인으로 3건을 흘려보내도 마지막 1건만 처리된다
function Test-AppServerBad {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string[]]$ComputerName
)
# begin/process/end를 하나도 쓰지 않으면, 본체는 통째로 end 블록 취급이 되어 「마지막에 한 번」만 동작한다.
# 파이프라인의 요소는 1건씩 매개변수에 바인딩되므로, end에 도달한 시점에 남아 있는 것은 마지막 1건
foreach ($name in $ComputerName) {
[pscustomobject]@{ ComputerName = $name }
}
}
'SV01', 'SV02', 'SV03' | Test-AppServerBad # → SV03 한 줄만. SV01과 SV02는 조용히 버려진다
'SV01', 'SV02', 'SV03' | Test-AppServer # → 3줄 반환(앞서 나온 process 블록 있는 버전)
까다로운 점은 오류가 단 하나도 나지 않는다는 것입니다. 게다가 Test-AppServerBad -ComputerName 'SV01','SV02','SV03'처럼 인수로 넘긴 경우에는 3건 모두 올바르게 처리되므로, 인수 호출만으로 동작을 확인하고 있으면 알아챌 수 없습니다. 파이프라인 입력을 받는 함수를 작성했다면, 여러 건을 흘려보내는 확인을 반드시 하나 넣으십시오.
5. Get-Help와 -WhatIf가 작동하게 하기 ── 남에게 넘기기 위한 최소 조건
5.1. 주석 기반 도움말
PowerShell 사용자는 모르는 명령을 만나면 우선 Get-Help를 두드립니다. 자체 제작한 함수가 그 문화에 올라탈 수 있는지 여부는, 주석 기반 도움말을 작성해 두었는지로 결정됩니다. 특수한 키워드가 붙은 주석을 적어 두기만 하면, Get-Help가 표준 cmdlet과 같은 형식으로 도움말을 표시해 줍니다.3
자주 쓰는 키워드를 목록으로 정리합니다. 전부 쓸 필요는 없으며, 최소한은 .SYNOPSIS와 .EXAMPLE, 남에게 넘긴다면 .DESCRIPTION과 .PARAMETER까지, 라는 순서로 늘려 가면 충분합니다.3
| 키워드 | 무엇을 적는가 |
|---|---|
| .SYNOPSIS | 한 줄 요약. Get-Help의 맨 앞에 나온다 |
| .DESCRIPTION | 자세한 설명. 전제 조건이나 부작용은 여기에 |
| .PARAMETER 매개변수명 | 매개변수별 설명. 키워드 뒤에 매개변수 이름을 이어서 쓴다 |
| .EXAMPLE | 사용 예. 첫 줄에 실행할 명령, 이어지는 줄에 그 설명. 여러 개 작성 가능 |
| .INPUTS | 파이프라인으로 받을 수 있는 개체의 형식 |
| .OUTPUTS | 반환하는 개체의 형식 |
| .NOTES | 보충 설명. 작성자, 갱신일, 알려진 제한 사항 등 |
| .LINK | 관련 명령이나 URL. 첫 번째 URL이 Get-Help -Online의 이동 대상이 된다 |
5.2. SupportsShouldProcess와 -WhatIf
삭제·이동·설정 변경을 수행하는 함수에는 [CmdletBinding(SupportsShouldProcess)]를 선언합니다. 이것만으로 -WhatIf와 -Confirm 매개변수가 자동으로 추가되고, 본체에서는 $PSCmdlet.ShouldProcess()의 반환값으로 실제로 변경을 실행할지를 분기합니다.4
이 둘을 모두 담은, 넘길 수 있는 품질의 함수 완성형은 다음과 같습니다.
function Remove-OldAppLog {
<#
.SYNOPSIS
지정한 폴더에서 보관 기한이 지난 로그 파일을 삭제합니다.
.DESCRIPTION
LastWriteTime이 보관 일수보다 오래된 *.log 파일을 삭제합니다.
-WhatIf로 삭제 대상 확인만 할 수 있습니다.
.EXAMPLE
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
삭제 대상을 표시하기만 하고, 실제로는 삭제하지 않습니다.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory)]
[ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
[string]$LogDir,
[ValidateRange(1, 3650)]
[int]$Days = 90
)
process {
$limit = (Get-Date).AddDays(-$Days)
# -File로 폴더를 제외한다(".log"로 끝나는 이름의 폴더를 잘못 지우지 않도록)
Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
Where-Object { $_.LastWriteTime -lt $limit } |
ForEach-Object {
# ShouldProcess가 $false를 반환하는 것은 -WhatIf일 때와 -Confirm에서 거부되었을 때
if ($PSCmdlet.ShouldProcess($_.FullName, "삭제")) {
Remove-Item -LiteralPath $_.FullName
}
}
}
}
Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIf라고 입력하면 「What if: …」 목록이 나올 뿐 아무것도 지워지지 않습니다. 변경계 스크립트를 남에게 넘길 때는 -WhatIf로 하는 리허설 절차를 함께 넘긴다── 이것이 이 사이트에서 거듭 권장하는 운영 방식입니다. 실제 로그 정리 스크립트에 적용한 예는 「PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기」에서 자세히 다룹니다.
참고로 호출한 앞단의 명령까지 -WhatIf가 항상 전파된다고 과신하지 말라는 주의사항도 공식 해설 문서에 있습니다. 확실하게 하려면 안쪽의 Remove-Item 등에 -WhatIf:$WhatIfPreference를 명시적으로 전달합니다.4
6. 공통 처리를 .psm1 모듈로 만들기
6.1. .psm1과 Export-ModuleMember
함수가 성장하다 보면, 여러 스크립트에서 같은 함수를 쓰고 싶어집니다. 복사·붙여넣기로 늘리면 수정이 모든 사본에 반영되지 않게 되므로, 공통 함수는 한데 모아 둡니다.
그 전 단계의 선택지가 닷 소싱(dot sourcing)입니다. 스크립트 경로 앞에 점과 공백을 붙여서 실행하면, 그 스크립트가 호출한 쪽의 범위(scope)에서 실행되어, 안에서 정의한 함수나 변수가 그대로 호출한 쪽에 남습니다.12
# 공통 함수를 작성한 Common.ps1을 불러온다(맨 앞의 점과 공백이 닷 소싱)
. C:\Scripts\Common.ps1
# Common.ps1 안에서 정의한 함수를 그대로 호출할 수 있다
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -WhatIf
간편하지만, 호출하는 쪽이 파일의 물리 경로를 알고 있어야 하고, 공개 범위도 고를 수 없습니다(함수도 변수도 별칭도 전부 그대로 흘러 들어옵니다). 스크립트 하나를 분할하는 정도라면 충분하지만, 두 번째 스크립트에서 같은 함수를 쓰고 싶어진 시점에 모듈화로 넘어가는 것이 판단 기준입니다.
모듈화하는 방법은 맥이 빠질 만큼 간단해서, 함수를 작성한 파일을 .psm1 확장자로 저장하기만 하면 됩니다.5
# AppOpsTools.psm1 ── 사내 운영 도구 공통 모듈
function Remove-OldAppLog { <# 앞 장의 함수 #> }
function Get-AppLogSummary { <# 집계 함수 #> }
# 내부 헬퍼 함수. 외부에는 노출하지 않는다
function ConvertTo-InternalPath { <# ... #> }
# 공개할 함수를 명시한다. 적지 않으면 모든 함수가 공개된다
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary
Export-ModuleMember를 적지 않으면, 모듈 안의 함수와 별칭은 모두 내보내집니다(변수는 내보내지지 않습니다). 생략 가능하지만, 공개 범위를 명시하는 것이 모범 사례로 여겨집니다.13 내부 헬퍼를 숨겨 두면, 나중에 자유롭게 리팩터링할 수 있는 여지가 남습니다.
6.2. 두는 위치 ── $env:PSModulePath와 자동 로드
모듈은 $env:PSModulePath에 나열된 폴더 아래에, 모듈 이름과 같은 이름의 폴더를 만들어서 둡니다(AppOpsTools\AppOpsTools.psm1). 폴더 이름과 파일의 기본 이름이 일치하지 않으면 모듈로 인식되지 않습니다.65 기본 저장 위치는 다음과 같으며, Windows PowerShell 5.1과 PowerShell 7에서 경로가 다르다는 점이 현장에서 흔히 걸려 넘어지는 부분입니다.6
| 범위 | PowerShell 7 | Windows PowerShell 5.1 |
|---|---|---|
| 자기 전용(CurrentUser) | $HOME\Documents\PowerShell\Modules |
$HOME\Documents\WindowsPowerShell\Modules |
| 전체 사용자(AllUsers) | $env:ProgramFiles\PowerShell\Modules |
$env:ProgramFiles\WindowsPowerShell\Modules |
표를 외우기보다 실제 환경에서 확인하는 편이 확실합니다. 한 줄로 볼 수 있습니다.
# 자신의 환경의 검색 경로를 한 항목당 한 줄로 확인한다(Windows의 구분 문자는 세미콜론)
$env:PSModulePath -split ';'
# 구분 문자를 환경에서 가져오는 방식. PowerShell 7에서 macOS/Linux도 다룬다면 이쪽
$env:PSModulePath -split [System.IO.Path]::PathSeparator
같은 $env:PSModulePath라는 이름이라도, 5.1과 7에서는 내용물이 별개입니다. 「모듈을 찾을 수 없다」의 원인은 대개 둔 위치가 그 환경의 검색 경로에 들어 있지 않은 것이므로, 먼저 이 한 줄을 실행해 보고 나서 의심하십시오.
올바른 위치에 두면, Import-Module을 적지 않아도 모듈 안의 명령을 처음 실행했을 때 PowerShell이 자동으로 임포트합니다(모듈 자동 로드).7 즉 사용자는 「명령이 처음부터 들어 있는 것처럼」 사용할 수 있습니다. 시행착오 중인 모듈은 전체 경로로 Import-Module해서 동작을 확인하고, 안정되면 PSModulePath 아래에 배치하는 흐름이 실무의 리듬입니다.
환경에 따라 달라지는 주의점이 두 가지 있습니다. Documents 폴더의 실체는 OneDrive의 폴더 리디렉션으로 이동해 있는 경우가 있으며, 그 경우 사용자 범위의 모듈도 OneDrive 아래에 놓입니다.6 또한 전체 사용자 범위에 배치하려면 관리자 권한이 필요합니다. 서버에 둔다면 AllUsers 범위로 하고, 작업 스케줄러의 실행 계정에서도 보이도록 해 두면, 「자기 단말에서는 동작하는데 서버에서는 동작하지 않는다」를 피할 수 있습니다.
6.3. psd1 매니페스트는 「배포하는 단계」에서
모듈 매니페스트(.psd1)는 모듈의 버전이나 의존 관계 등의 메타데이터를 기술하는 해시 테이블 파일로, 필수는 아닙니다. 매니페스트에서 필수인 키는 ModuleVersion뿐입니다.8 자신의 팀 안에서 사용하는 단계에서는 .psm1 단독으로 충분하며, 다른 부서에 배포하거나 버전 관리를 엄격히 하는 단계가 되면 New-ModuleManifest로 생성합니다.8
New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
-RootModule 'AppOpsTools.psm1' `
-ModuleVersion '1.0.0' `
-FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
-PowerShellVersion '5.1'
생성된 psd1은 주석이 달린 템플릿으로 되어 있으므로, 필요한 키만 키워 나가면 됩니다.8
6.4. 사내 공유와 버전 관리의 요령
- 원본은 Git에 둡니다. 스크립트와 모듈은 텍스트이므로 Git과 궁합이 좋고, 「언제, 누가, 왜 바꿨는가」를 추적할 수 있다는 점이 운영 스크립트의 신뢰성 그 자체가 됩니다. ModuleVersion의 갱신을 커밋과 대응시켜 두면, 서버에 들어 있는 버전을 특정하기 쉬워집니다.
- 배포는 「공유 폴더에서 각 머신의 PSModulePath 아래로 복사」가 기본형입니다. 모듈 폴더째로 복사하면 수동 설치할 수 있습니다.7 공유 폴더 상의 경로를 PSModulePath에 직접 추가하는 구성은, 공유가 「느리다·끊긴다·없을 때가 있다」는 전제(자세한 내용은 「네트워크 드라이브와 UNC 경로의 함정」)를 고려하면 상시 사용은 권장하지 않습니다.
- 실행 정책과의 관계에 주의합니다. 기본값인 RemoteSigned는 로컬에서 만든 서명 없는 스크립트를 허용하지만, UNC 경로와 인터넷 경로를 구분하지 않는 시스템에서는 UNC 경로 상의 스크립트가 실행을 거부당하는 경우가 있습니다. 또한 다운로드 유래로 표시가 붙은 파일은 차단되며, Unblock-File로 해제하거나 서명이 필요합니다.9 사내 배포를 본격화한다면, 코드 서명과의 조합을 「PowerShell의 실행 정책과 스크립트 서명」에서 확인하십시오.
7. 실무의 정석(판단표)
| 논점 | 선택지 | 판단 기준 |
|---|---|---|
| 인수 받기 | 변수 직접 적기 / param 블록 | 2회 이상 사용하거나 다른 사람이 사용한다면 param이 정답. 기본값은 「안전한 쪽」으로 잡는다2 |
| [CmdletBinding()] | 붙이지 않는다 / 붙인다 | 남에게 넘기는 것·운영에 올리는 것은 항상 붙인다. 오타가 실행 전에 멈춘다1 |
| 입력 체크 | 본체의 if문 / 검증 속성 | 단일 매개변수의 형식 체크는 속성으로. 조합 검증만 본체에서2 |
| 변경계의 안전장치 | 독자적인 -TestMode 인수 / SupportsShouldProcess | 자체 플래그는 만들지 않는다. 표준 -WhatIf/-Confirm에 올라탄다4 |
| 공통 처리를 두는 방식 | 복사·붙여넣기 / 닷 소싱 / .psm1 모듈 | 두 번째 스크립트에서 공유하는 시점에 모듈화. 공개 함수는 Export-ModuleMember로 명시513 |
| 모듈을 두는 위치 | 임의 폴더+Import-Module / PSModulePath 아래 | 상시 사용하는 것은 정해진 위치에 두어 자동 로드에 태운다. 서버는 AllUsers 범위67 |
| psd1 매니페스트 | 처음부터 만든다 / 배포하는 단계에서 만든다 | 팀 밖으로 내보내거나 버전 관리를 엄격히 하는 단계에서 New-ModuleManifest8 |
8. 정리
- param 블록과 [CmdletBinding()]으로 고급 함수로 만드는 것이 「남에게 넘길 수 있는 스크립트」의 출발점입니다. 공통 매개변수가 붙고, 인수 실수가 실행 전에 멈춥니다.
- 형식 지정·[Parameter(Mandatory)]·안전한 쪽의 기본값·검증 속성으로, 오류를 입구에서 빨리 냅니다. ValidateSet은 탭 완성이라는 편의성도 높여 줍니다.
- 파이프라인 입력은 ValueFromPipeline과 process 블록을 세트로 받습니다. process를 잊으면 마지막 1건만 처리됩니다.
- 주석 기반 도움말로 Get-Help가 작동하게 하고, 변경계는 SupportsShouldProcess로 -WhatIf에 대응시킵니다. 리허설할 수 있다는 것이 운영의 안전장치가 됩니다.
- 공통 함수는 .psm1로 분리해, Export-ModuleMember로 공개 범위를 명시하고, PSModulePath 아래에 배치합니다. 5.1과 7에서 경로가 다르다는 점에 주의하십시오.
- psd1 매니페스트는 배포하는 단계에서. 원본은 Git으로 관리하고, 공유 폴더 배포에서는 실행 정책(UNC 경로와 RemoteSigned의 관계)을 확인합니다.
관련 글
- PowerShell 명령어의 기초 — 가장 먼저 익힐 조작과 안전한 사용법
- PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기
- Pester로 하는 PowerShell 테스트 정비 ── 운영 스크립트를 쉽게 망가지지 않게 만드는 실무 패턴
- PowerShell의 실행 정책과 스크립트 서명 ── ‘Bypass로 덮어버리는’ 운영에서 벗어나는 실무 가이드
- PowerShell의 에러 처리와 재실행 설계 ── try/catch가 먹히지 않는 함정부터 exit code·재시도의 정석까지
- PowerShell에서 자격 증명을 안전하게 다루기 ── 평문 비밀번호를 스크립트에서 추방한다
관련 상담 영역
합동회사 코무라소프트에서는 특정인에게 의존하게 된 PowerShell 스크립트의 정리·모듈화, 운영 자동화 스크립트의 설계 리뷰, 사내 배포·버전 관리 체계 구축을 다루고 있습니다. 「동작은 하지만 아무도 손댈 수 없는」 스크립트 자산의 재고 조사 단계부터도 상담하실 수 있습니다.
참고 링크
-
Microsoft Learn, about_Functions_CmdletBindingAttribute. CmdletBinding 속성이 함수를 컴파일된 cmdlet처럼 동작시킨다는 점, 공통 매개변수가 자동으로 추가된다는 점, $PSCmdlet을 사용할 수 있게 된다는 점, 알 수 없는 매개변수나 대응하지 않는 위치 인수에서 바인드가 실패한다는 점, SupportsShouldProcess가 Confirm/WhatIf 매개변수를 추가한다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Functions_Advanced_Parameters. Parameter 속성과 Mandatory, ValueFromPipeline, switch 매개변수, ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty/ValidatePattern 등 검증 속성의 사양, 검증 실패 시 함수가 호출되지 않는다는 점, 속성을 형식보다 앞에 선언하는 것이 모범 사례라는 점, ValidateScript의 ErrorMessage 인수가 PowerShell 6 이후라는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15
-
Microsoft Learn, about_Comment_Based_Help. .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE 등의 키워드로 주석 기반 도움말을 작성하면 Get-Help가 XML 도움말과 같은 형식으로 표시한다는 점, 스크립트와 함수 각각에서의 배치 규칙에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Everything you wanted to know about ShouldProcess. SupportsShouldProcess 지정만으로 -WhatIf/-Confirm이 자동으로 만들어진다는 점, $PSCmdlet.ShouldProcess()로 분기하는 방법, -WhatIf의 전파를 과신하지 말고 안쪽 명령에 명시적으로 전달하는 것이 권장된다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to Write a PowerShell Script Module. .psm1 확장자로 저장하기만 하면 스크립트 모듈이 된다는 점, 스크립트와 같은 이름의 폴더에 저장한다는 점, 기본적으로 모든 함수가 공개되고 변수는 공개되지 않는다는 점, Export-ModuleMember로 공개할 함수를 명시하는 것이 권장된다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_PSModulePath. $env:PSModulePath가 모듈 검색 폴더 목록이라는 점, PowerShell 7과 Windows PowerShell 5.1에서 CurrentUser/AllUsers 범위의 기본 경로가 다르다는 점, OneDrive나 폴더 리디렉션으로 Documents의 위치가 바뀔 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Modules. PSModulePath 아래의 모듈은 명령을 처음 실행할 때 자동으로 임포트된다는 점(모듈 자동 로드), 모듈 폴더째로 복사하는 수동 설치 방법, 기본 모듈 배치 위치에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, New-ModuleManifest. 모듈 매니페스트(.psd1)가 모듈의 내용·속성·전제 조건을 기술하는 해시 테이블이며 필수가 아니라는 점, 필수 키가 ModuleVersion뿐이라는 점, New-ModuleManifest가 템플릿으로 쓸 수 있는 틀을 생성한다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, about_Execution_Policies. RemoteSigned가 로컬에서 만든 서명 없는 스크립트를 허용하고 인터넷에서 유래한 스크립트에는 서명을 요구한다는 점, UNC 경로와 인터넷 경로를 구분하지 않는 시스템에서는 UNC 경로 상의 스크립트가 RemoteSigned에서 실행을 허용받지 못하는 경우가 있다는 점, Unblock-File을 통한 차단 해제에 대해. ↩ ↩2
-
Microsoft Learn, about_Functions_Advanced_Methods. 고급 함수에서 사용할 수 있는 begin/process/end 입력 처리 메서드, ShouldProcess 메서드가 process 블록 안에서 호출되며 CmdletBinding 속성으로의 선언이 필요하다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Approved Verbs for PowerShell Commands. 명령 이름이 Verb-Noun 형식이어야 한다는 점, 승인된 동사 목록과 Get-Verb로 확인하는 방법, 승인되지 않은 동사를 포함한 모듈을 임포트할 때 경고가 표시된다는 점에 대해. ↩
-
Microsoft Learn, about_Scripts. 스크립트는 기본적으로 독자적인 범위에서 실행되며, 그 안에서 만들어진 함수·변수·별칭·드라이브가 스크립트 범위에만 존재한다는 점, 경로 앞에 점과 공백을 붙여 실행하는 「닷 소싱」을 사용하면 현재 범위에서 실행되어 만들어진 항목이 실행 후에도 세션에 남는다는 점에 대해. ↩
-
Microsoft Learn, Export-ModuleMember. Export-ModuleMember가 스크립트 모듈에서 내보낼 멤버를 지정하는 cmdlet이라는 점, 지정하지 않으면 함수와 별칭이 내보내지고 변수는 내보내지지 않는다는 점, 생략 가능하지만 작성자의 의도를 나타내는 모범 사례라는 점에 대해. ↩ ↩2
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PowerShell에서 COM과 .NET을 호출하는 실전 ── 스크립트가 닿는 범위를 단숨에 넓힌다
PowerShell에서 .NET 클래스를 호출하는 방법, Add-Type을 통한 C#과 Win32 API 통합, COM 조작, Excel 프로세스 잔류와 뒷정리, Office 무인 실행이 비지원인 이유, 5.1과 7의 차이까지 실무 관점에서 해설...
Windows PowerShell 5.1과 PowerShell 7의 차이 ── 사내 스크립트 이행 실무 가이드
Windows PowerShell 5.1과 PowerShell 7의 관계(공존과 pwsh.exe), 5.1은 신기능을 추가하지 않는다는 공식 방침, 인코딩 차이로 인한 문자 깨짐, #Requires를 통한 방어, 작업 스케줄러 갱신까지 이행 절차...
PowerShell로 Excel・CSV 업무 처리를 자동화한다 ── 집계・대조・장표 출력의 실무 레시피
PowerShell로 CSV 집계・대조와 Excel 장표 출력을 자동화하는 실무 레시피입니다. Import-Csv/Export-Csv의 문자 코드 기본값(5.1과 7의 차이), Group-Object 집계, Compare-Object와 해시테이블...
PowerShell 실용 명령어 모음 ── 일상 업무에서 자주 쓰는 작은 기능 늘리기
PowerShell로 일상 업무에 쓰는 실용 명령어로서, Measure-Object, Group-Object, Select-String, Compare-Object, Tee-Object, Start-Transcript 등의 활용 시점을 정리합니다.
winget + PowerShell로 PC 키팅을 자동화하기 ── 절차서를 실행 가능하게 만들기
신입사원 PC 셋업을 재현 가능하게 만드는 방법을 정리합니다. winget을 통한 앱 도입과 export/import, WinGet Configuration의 선언적 구성, PowerShell로 보완하는 설정, 무인 실행 시 주의점까지 해설합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기존 자산 활용 & 이관 지원
COM / ActiveX / OCX 자산, 네이티브 코드, 32비트 의존성을 유지하면서 단계적인 이관 계획을 지원합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- PowerShell의 param 블록에 [CmdletBinding()]을 붙이면 무엇이 달라지나요?
- 함수나 스크립트가 「고급 함수(advanced function)」로 취급되어, 컴파일된 cmdlet과 같은 동작을 갖추게 됩니다. 구체적으로는 -Verbose나 -ErrorAction 같은 공통 매개변수가 자동으로 추가되고, $PSCmdlet 변수를 사용할 수 있게 되며, 정의되지 않은 매개변수나 여분의 위치 인수를 넘기면 바인드 오류가 됩니다. 오타가 난 인수가 조용히 무시되지 않게 되므로, 운영 스크립트에서는 붙이는 것이 기본입니다.
- 스크립트의 인수 체크는 Validate 속성과 if문 중 어느 쪽으로 작성해야 하나요?
- 매개변수의 형식 체크는 ValidateSet이나 ValidateRange, ValidateScript 같은 검증 속성으로 몰아주는 것이 정석입니다. 검증은 함수 본체가 실행되기 전에 이루어지며, 값이 올바르지 않으면 처리가 한 줄도 실행되지 않은 채 오류가 되므로, 「도중까지 실행되다가 망가지는」 사고를 막을 수 있습니다. ValidateSet에는 탭 완성이 작동한다는 실익도 있습니다. 한편 여러 매개변수의 조합 검증이나 실행 시점의 상태에 의존하는 체크는 본체 쪽의 if문으로 처리합니다.
- PowerShell의 자체 제작 모듈(.psm1)은 어디에 두면 되나요?
- $env:PSModulePath에 포함된 폴더 아래에 「모듈 이름과 같은 이름의 폴더」를 만들어서 둡니다. 자기 전용이라면 PowerShell 7에서는 $HOME\Documents\PowerShell\Modules, 전체 사용자 공용이라면 $env:ProgramFiles\PowerShell\Modules가 기본값입니다. Windows PowerShell 5.1에서는 각각 WindowsPowerShell\Modules로 경로가 다르다는 점에 주의하십시오. 이 위치에 두면 Import-Module을 적지 않아도 명령을 처음 실행할 때 자동으로 로드됩니다.
- 모듈 매니페스트(psd1)는 반드시 만들어야 하나요?
- 필수는 아닙니다. 매니페스트 없이 .psm1 단독으로도 모듈로서 동작합니다. 매니페스트가 필요해지는 시점은 「배포하는 단계」로, 버전 번호, 필요한 PowerShell 버전, 의존 모듈, 내보낼 명령의 명시 등 메타데이터를 갖추고 싶을 때 New-ModuleManifest로 생성합니다. 매니페스트에서 필수인 키는 ModuleVersion뿐이므로, 우선 최소 구성으로 시작해 필요에 따라 키워 나가면 충분합니다.
- 사내 공유 폴더에 둔 스크립트가 실행 정책 때문에 차단되는 이유는 무엇인가요?
- 기본값인 RemoteSigned 정책은 로컬에서 만든 스크립트는 서명이 없어도 실행을 허용하지만, 인터넷에서 유래한 것으로 표시된 스크립트에는 서명을 요구합니다. UNC 경로와 인터넷 경로를 구분하지 않는 구성의 시스템에서는 공유 폴더 상의 스크립트가 RemoteSigned에서 실행을 거부당하는 경우가 있다고 공식 문서에도 명시되어 있습니다. 사내 배포를 본격화한다면 코드 서명과 AllSigned의 조합, 또는 인트라넷 영역 구성을 검토하십시오.