수정 이력(4건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
- 글 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
- 외부 리뷰(1283건)에 대응해 본문을 업데이트했습니다. 개별 변경 내용은 아래 이력을 참고하세요.
- 작업 스케줄러 「동작」 탭의 입력 예를 3열 표로 추가했습니다. `process` 블록을 작성하지 않은 경우의 NG 예(3건을 흘리면 마지막 1건만 처리되고, 게다가 오류가 나지 않음)를 대비해 넣고, 주석 기반 도움말 키워드 표, 도트 소싱 설명과 모듈화와의 구분, `$env:PSModulePath` 확인 명령을 덧붙였습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22174750)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「PowerShell 스크립트의 인수 설계와 모듈화 ── 「돌아가는 스크립트」에서 「남에게 넘길 수 있는 스크립트」로」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-param-module-design/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22174750
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22174751
「담당자가 작성한 PowerShell 스크립트는 돌아가지만, 그 사람만 손댈 수 있다」「서버 이름이나 경로가 코드 곳곳에 직접 박혀 있어, 환경이 바뀔 때마다 본체를 고친다」「인수를 잘못 넘겨도 조용히 실행되고, 나중에야 알아챈다」── 운영 자동화 상담에서, 스크립트 자체보다 「스크립트를 넘기는 방법·키우는 방법」이 문제가 되는 경우는 매우 많습니다.
개인 PC에서만 돌린다면 변수를 직접 박아 넣은 「돌아가는 스크립트」로도 충분합니다. 그러나 작업 스케줄러에 올리거나, 동료에게 넘기거나, 여러 서버에서 재사용하게 되는 순간, 인수 설계와 공통 처리 정리가 품질을 가릅니다. 다행히 PowerShell에는 이 「남에게 넘길 수 있는 스크립트」로 가는 도구가 언어 기능으로 처음부터 마련되어 있습니다. param 블록, 검증 특성, 주석 기반 도움말, 그리고 모듈입니다.
이 글에서는 중소기업 IT·운영 담당자가 이미 가지고 있는 「돌아가는 스크립트」를 출발점으로, 인수 설계 → 입력 검증 → 도움말과 -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
그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 18건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle
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. 입력 검증은 입구에서 ── Validate 특성으로 「오류를 빨리」
인수의 형식 검사를 함수 본문의 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 블록이 없으면, 파이프라인으로 여러 값을 흘려도 마지막 한 건만 처리되는 전형적인 버그가 됩니다.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 블록으로 취급되어 「마지막에 한 번」만 돈다.
# 파이프라인 요소는 한 건씩 매개변수에 바인딩되므로, end에 왔을 때 남아 있는 것은 마지막 한 건
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
함수가 커지면, 여러 스크립트에서 같은 함수를 쓰고 싶어집니다. 복사해 늘리면 수정이 모든 복사본에 퍼지지 않으므로, 공통 함수는 한데 모읍니다.
그 앞 단계의 선택지가 도트 소싱입니다. 스크립트 경로 앞에 점과 공백을 붙여 실행하면, 그 스크립트가 호출 측 스코프에서 실행되어, 안에서 정의한 함수나 변수가 그대로 호출 측에 남습니다.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 범위로 해서, 작업 스케줄러의 실행 계정에서도 보이게 해 두면, 「내 PC에서는 되는데 서버에서는 안 된다」를 피할 수 있습니다.
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 블록 | 두 번 이상 쓰거나 다른 사람이 쓰면 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를 빼먹으면 마지막 한 건만 처리됩니다.
- 주석 기반 도움말로 Get-Help가 통하게 하고, 변경을 수행하는 함수는 SupportsShouldProcess로 -WhatIf를 지원합니다. 리허설할 수 있는 것이 운영의 안전장치가 됩니다.
- 공통 함수는 .psm1로 분리하고, Export-ModuleMember로 공개 범위를 명시한 뒤, PSModulePath 아래에 배치합니다. 5.1과 7에서 경로가 다르다는 점에 주의하세요.
- psd1 매니페스트는 배포하는 단계에서. 원본은 Git으로 관리하고, 공유 폴더 배포에서는 실행 정책(UNC 경로와 RemoteSigned의 관계)을 확인합니다.
관련 글
- PowerShell 명령의 기본 ── 먼저 익힐 조작과 안전한 사용법
- PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트를 안전하게 자동화하기
- Pester로 하는 PowerShell 테스트 정비 ── 운영 스크립트를 잘 깨지지 않게 하는 실무의 틀
- PowerShell의 실행 정책과 스크립트 서명
- PowerShell의 오류 처리와 재실행 설계
- PowerShell에서 자격 정보를 안전하게 다루기 ── 평문 비밀번호를 스크립트에서 없애기
관련 상담 영역
KomuraSoft LLC에서는, 특정 담당자에게만 의존하는 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의 조합, 또는 인트라넷 영역 구성을 검토하세요.