Write-Host를 그만두기 ── PowerShell의 출력 스트림과 로그 설계

· 업데이트: · · PowerShell, Windows, 로그, 운영 개선, 자동화, 스크립트, 설계, 유지보수성

수정 이력(6건, 최종 수정 2026년 09월 03일)

이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
글 앞머리에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참고하세요.
전제로 하는 버전과, Windows PowerShell 5.1에서 주의할 지점을 앞머리 표로 정리했습니다. 이와 함께 6가지 스트림이 어디로 흐르는지 보여주는 그림과, 리다이렉트가 적용됐는지 확인하는 절차를 추가했습니다.
Write-Host가 -InformationAction의 영향을 받지 않는다는 설명에, 유일한 예외인 -InformationAction Ignore에 대한 단서를 보완했습니다. 관련 글 링크 문구도 현재 제목에 맞췄습니다.
정보 스트림의 기본값과 「Write-Host는 기본적으로 화면에 나온다」는 설명이 모순처럼 읽히는 점에 대해, Write-Host만 예외인 이유를 보완했습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22175000)

아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.

Go Komura (2026). 「Write-Host를 그만두기 ── PowerShell의 출력 스트림과 로그 설계」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-output-streams-logging/

DOI(등록된 아카이브)
10.5281/zenodo.22175000
DOI(마지막 등록 버전)
10.5281/zenodo.22175001

「스크립트는 돌아가는데, 실패했을 때 무슨 일이 있었는지 모른다」── 운영에 올린 PowerShell 스크립트에서 가장 많은 상담이 이것입니다. 원인을 쫓다 보면 대개 같은 구조에 닿습니다. 처리 상황이 Write-Host로만 표현되어 있어, 화면을 보고 있지 않던 야간 실행에는 아무것도 남아 있지 않다는 것입니다.

PowerShell에는 출력 스트림이 6종류 있고, 각각 「누구를 향한 정보인가」가 다릅니다. 값을 반환할 것인지, 사람이 읽을 것인지, 조사할 때만 필요한 것인지. 이를 의식해 나눠 쓰면 같은 스크립트가 대화 실행에서는 친절하게, 무인 실행에서는 기계가 읽을 수 있게 동작합니다. 반대로 전부 Write-Host로 흘리면, 값으로도 못 쓰고 로그에도 안 남는 정보만 늘어납니다.

이 글에서는 6가지 스트림의 역할, Write-Host의 올바른 자리, 함수 반환값이 오염되는 구조, 호출 측에서 상세도를 제어하는 방법, 그리고 운영에서 쓸 수 있는 구조화 로그의 형태까지 정리합니다.

1. 먼저 결론

  • PowerShell 출력 스트림은 6개입니다. 성공(1)·에러(2)·경고(3)·상세(4)·디버그(5)·정보(6)이며, 각각 번호로 리다이렉트할 수 있습니다. *>는 모든 스트림입니다.1
  • PowerShell 5.0 이후 Write-Host는 정보 스트림에 씁니다. 그래서 6>로 리다이렉트하거나 -InformationVariable로 잡을 수 있게 되었습니다. 그 이전에는 잡지도 막지도 못했습니다.2
  • Write-Host는 「사람에게 보여주는 표시」 전용입니다. 값을 반환하는 용도로는 쓸 수 없습니다. 파이프라인에 넘길 값은 Write-Output(또는 그대로 출력)입니다.23
  • 함수는 내부에서 출력된 모든 객체를 반환합니다. return 유무는 관계없습니다. 불필요한 출력은 $null = ...로 버리는 것이 정석입니다.4
  • 진행은 Write-Progress, 처리 경과는 Write-Verbose입니다. 진행 표시는 리다이렉트 가능한 데이터 스트림이 아닙니다.5
  • [CmdletBinding()]을 붙인 함수는 -Verbose -Debug -InformationAction 같은 공통 매개변수를 자동으로 갖습니다. 호출 측이 상세도를 정할 수 있는 형태가 정답입니다.67
  • 기본값은 기억해 둘 가치가 있습니다. $VerbosePreference $DebugPreference $InformationPreference는 SilentlyContinue, $WarningPreference $ErrorActionPreference는 Continue입니다.8
  • 나중에 분석할 로그는 구조화(한 줄에 JSON 하나)합니다. 화면 모습을 재현하고 싶다면 Start-Transcript를 같이 씁니다.9

이 글이 전제로 하는 버전과 5.1에서의 차이

정보시스템 현장에서는 Windows PowerShell 5.1이 여전히 주류입니다. 이 글의 서술이 어느 버전을 전제로 하는지 먼저 정리합니다.

서술 전제 버전 Windows PowerShell 5.1에서는
6가지 출력 스트림과 번호 달린 리다이렉트, *> 5.1에서도 PowerShell 7에서도 동일 그대로 해당합니다1
Write-Host가 정보 스트림(6번)에 쓴다(6>-InformationVariable로 잡을 수 있음) PowerShell 5.0 이후(3장) 5.1은 5.0 이후이므로 그대로 해당합니다2
Write-Information-InformationAction / -InformationVariable PowerShell 5.0 이후(4장) 그대로 해당합니다7
함수가 내부 출력을 모두 반환하는 것, $null = ...로 억제 버전에 의존하지 않습니다(5장) 그대로 해당합니다4
네이티브 명령의 stdout을 파일로 직접 리다이렉트할 때 바이트 보존 PowerShell 7.4 이후(6장) 바이트 보존을 전제로 할 수 없습니다. 2>&1 이후의 타입은 버전과 함께 변수·파이프라인·파일 중 어디서 받는지도 확인하세요
Write-Progress-ProgressAction 공통 매개변수로 제어 PowerShell 7.4 이후5 쓸 수 없습니다. $ProgressPreference로 제어합니다(8장은 이쪽으로 썼습니다)
글 말미에서 배포하는 샘플 코드 PowerShell 7.6에서 실행해 검증(Pester 14건)

요컨대 2장~5장과 7장은 5.1에서도 그대로 쓸 수 있습니다. 버전을 의식해야 하는 곳은 외부 명령 리다이렉트(6장)와 진행 표시 제어 방법(8장), 이 두 곳뿐입니다.

그림의 실선은 항상 성립하는 관계, 점선은 조건이 붙는 관계입니다(성립 조건은 상세 페이지의 관계별 설명에 적혀 있습니다). 관계 전체 목록(총 20건, 근거와 확신도 포함)과 주요 개념의 정의는 지식 맵 상세 페이지에 정리되어 있습니다(일본어). 데이터: JSON-LD / Turtle

2. 6가지 스트림과 그 상대

먼저, 어느 쓰기 명령이 어디로 도착하는지를 한 장으로 정리합니다.

Write-Output / 그대로 출력Write-Error / Write-WarningWrite-Verbose / Write-DebugWrite-Information / Write-Host1 성공 스트림2 에러 / 3 경고 / 4 상세5 디버그 / 6 정보후속 처리로 넘어감파이프라인·변수 대입화면에 나옴기본으로 나오는지는 기본 설정 변수에 따름파일에 남길 수 있음번호 달린 리다이렉트·포착용 변수

후속 처리로 넘어가는 것은 성공 스트림뿐입니다. 나머지 다섯은 화면에 나오거나, 리다이렉트나 -*Variable로 잡거나 둘 중 하나이며, 파이프라인에는 실리지 않습니다. 왼쪽 분기(어느 명령으로 쓸 것인가)를 잘못하면, 값이 안 오거나 로그에 안 남는 형태로 반드시 겉으로 드러납니다. 번호는 3> warnings.log처럼 리다이렉트를 지정할 때 씁니다(6장).

# 스트림 쓰는 명령 대상 독자 기본 설정
1 성공(Success) Write-Output / 그대로 출력 후속 처리(파이프라인)
2 에러(Error) Write-Error / throw 사람 + 감시 Continue
3 경고(Warning) Write-Warning 사람 Continue
4 상세(Verbose) Write-Verbose 조사 중인 사람 SilentlyContinue
5 디버그(Debug) Write-Debug 개발자 SilentlyContinue
6 정보(Information) Write-Information / Write-Host 사람 + 기록 SilentlyContinue

정보 스트림 기본값이 SilentlyContinue인데 Write-Host가 화면에 나오는 것은 모순이 아닙니다. Write-Host만 예외이며, Microsoft Learn은 「$InformationPreference 기본 설정 변수와 -InformationAction 공통 매개변수는 Write-Host 메시지에 영향을 주지 않는다」고 명시합니다.2Write-Information은 기본으로 나오지 않지만, Write-Host는 기본으로도 표시되고, 막을 수 있는 것은 -InformationAction Ignore6> 리다이렉트뿐입니다.

이 표에서 가장 중요한 것은 첫 행입니다. 성공 스트림은 「사람용 메시지」를 쓰는 자리가 아닙니다. 여기에 사람용 문자열을 쓰면, 그 함수를 |로 잇는 순간 후속 처리에 예상 밖 문자열이 흘러 들어갑니다.

function Get-KsTargetFile {
    Write-Output "대상을 검색하는 중입니다..."   # 【NG】반환값에 섞임
    Get-ChildItem -Path $path -Filter '*.csv'
}

# 호출 측은 FileInfo 배열을 기대하는데, 맨 앞에 문자열이 들어온다
$files = Get-KsTargetFile
$files[0].FullName    # → 비어 있음(맨 앞이 문자열이므로)

올바른 방법은 경과 보고를 상세 스트림이나 정보 스트림으로 보내는 것입니다.

function Get-KsTargetFile {
    [CmdletBinding()]
    param([string] $Path)

    Write-Verbose "대상을 검색하는 중입니다: $Path"   # -Verbose 지정 시에만 표시
    Get-ChildItem -Path $Path -Filter '*.csv'      # 반환값은 FileInfo만
}

3. Write-Host는 「악」인가

한때 「Write-Host는 쓰지 마라」는 주장이 퍼졌지만, 지금의 PowerShell에서는 사정이 달라졌습니다. PowerShell 5.0 이후 Write-Host는 정보 스트림(6번)에 쓰는 구현이라, 6>로 리다이렉트하거나 -InformationVariable로 잡을 수 있습니다.2 「화면에만 나오고 나중에 전혀 잡을 수 없다」는 당시 비판은 더 이상 맞지 않습니다. 다만 2장에서 말했듯, 정보 스트림 기본값이 SilentlyContinue여도 Write-Host 표시만은 사라지지 않습니다. $InformationPreference-InformationAction의 영향을 받지 않기 때문입니다(유일한 예외는 -InformationAction Ignore이며, 이것만 Write-Host 출력도 막습니다). 「잡을 수 있게 된 것」과 「기본으로 화면에 나오는 것」은 함께 성립합니다.2

그렇다고 쓰임새가 넓은 것은 아닙니다.

Write-Host가 맞는 장면

  • 대화형으로 쓰는 도구에서 색이 있는 제목이나 구분선을 내고 싶을 때(-ForegroundColor)
  • 「이 스크립트가 이제 무엇을 할지」를 사용자에게 전할 때
  • 처리 값이 아니라, 꾸민 표시 자체가 목적일 때

Write-Host를 쓰면 안 되는 장면

  • 함수 반환값으로 값을 넘기고 싶을 때(→ Write-Output)
  • 나중에 분석할 운영 로그를 남기고 싶을 때(→ 구조화 로그, 또는 Write-Information)
  • 호출 측에서 표시·비표시를 켜고 끄고 싶을 때(→ Write-Verbose)

무인 실행 스크립트에는 애초에 표시할 곳이 없습니다. Write-Host만으로 상황을 표현한 스크립트는 작업 스케줄러에서 실행되는 순간 「아무것도 알 수 없는 스크립트」가 됩니다. 여기가 이 글 제목의 의미입니다.

4. 호출 측에 제어를 넘긴다 ── [CmdletBinding()]과 공통 매개변수

Write-Verbose의 진가는 표시 여부를 호출 측이 정할 수 있다는 점에 있습니다. 함수에 [CmdletBinding()]만 붙이면 -Verbose -Debug -WarningAction -InformationAction -ErrorAction 같은 공통 매개변수를 자동으로 쓸 수 있게 됩니다.67

function Invoke-KsImport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $CsvPath
    )

    Write-Verbose "가져오기 시작: $CsvPath"          # 기본으로는 안 나옴
    Write-Information "가져오기: $CsvPath" -Tags 'KsImport'   # 기본으로는 안 나옴(포착은 가능)

    $rows = Import-Csv -Path $CsvPath
    if ($rows.Count -eq 0) {
        Write-Warning "$CsvPath 에 가져올 대상이 없습니다"   # 기본으로 나옴
        return
    }

    Write-Verbose "$($rows.Count) 건을 처리합니다"
    $rows | ForEach-Object { ConvertTo-KsRecord $_ }         # 반환값은 이것만
}

# 보통 실행: 경고만 표시되고, 반환값은 레코드
$records = Invoke-KsImport -CsvPath 'D:\in\orders.csv'

# 조사 시: 경과도 보고 싶다
$records = Invoke-KsImport -CsvPath 'D:\in\orders.csv' -Verbose

# 정보 스트림만 변수에 잡아 나중에 로그 파일로
$records = Invoke-KsImport -CsvPath 'D:\in\orders.csv' -InformationVariable info
$info | ForEach-Object { $_.MessageData } | Add-Content -Path $logPath

자체 $LogLevel 변수를 만들어 if로 나누는 구현을 자주 보지만, 표준 체계에 올리는 편이 짧고 다른 사람에게도 의도가 전달됩니다. -Verbose를 붙이면 상세가 나온다는 것은 PowerShell을 쓰는 사람 전원의 공통 인식이기 때문입니다.

참고로 $VerbosePreference 같은 기본 설정 변수는 스코프와 자식 스코프에 적용됩니다.8 함수를 -Verbose와 함께 호출하면 그 안에서 호출되는 cmdlet도 상세 출력을 시작하므로, 생각보다 출력이 늘어날 수 있습니다.

5. 함수 반환값이 오염된다 ── PowerShell 특유의 함정

PowerShell 함수는 명시적인 return이 없어도, 내부에서 출력된 모든 객체를 반환합니다.4 강력한 사양인 한편, 사고가 가장 많은 부분이기도 합니다.

function New-KsWorkFolder {
    param([string] $Path)

    New-Item -Path $Path -ItemType Directory   # 【함정】DirectoryInfo가 반환값에 섞임

    $list = [System.Collections.Generic.List[string]]::new()
    $list.Add('log')                            # 【함정2】.Add()는 void라 실해가 없음
    $sb = [System.Text.StringBuilder]::new()
    $sb.Append('x')                             # 【함정3】StringBuilder 자신이 반환됨

    return $Path
}

$p = New-KsWorkFolder -Path 'D:\work'   # $p는 3요소 배열이 된다(DirectoryInfo, StringBuilder, string)

대처는 「불필요한 출력을 버리는 것」입니다. 쓰기는 세 가지인데, $null = ...가 가장 가볍습니다.

$null = New-Item -Path $Path -ItemType Directory   # 권장
New-Item -Path $Path -ItemType Directory | Out-Null # 파이프만큼 느림
[void] $sb.Append('x')                             # .NET 메서드에 자주 쓰임

이 동작은 테스트를 쓰면 한 번에 알아챕니다. 「반환값 형태를 고정하는」 테스트의 중요성은 「Pester로 PowerShell 테스트 정비하기」에서 다룹니다.

6. 리다이렉트와 포착

스트림은 번호로 따로 리다이렉트할 수 있습니다.1

.\Invoke-NightlyExport.ps1 3> warnings.log            # 경고만 다른 파일로
.\Invoke-NightlyExport.ps1 4>&1 | Tee-Object -FilePath run.log   # 상세를 성공 스트림에 합류
.\Invoke-NightlyExport.ps1 *> all.log                 # 모든 스트림을 한 파일로
.\Invoke-NightlyExport.ps1 2>&1 | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }

>는 덮어쓰기, >>는 이어 쓰기입니다. 다만 리다이렉트로 합류시키면 타입이 섞인다는 점은 의식하세요. 위 예처럼 PowerShell 스크립트나 함수의 에러 스트림을 합류시킨 경우, 그 요소는 ErrorRecord 그대로이므로 위와 같이 타입으로 가를 수 있습니다.

외부 프로그램(네이티브 명령)은 파일로 직접 리다이렉트하는 경우와 PowerShell 안에서 객체로 받는 경우를 구분해야 합니다. PowerShell 7.4 이후에는 네이티브 명령의 stdout을 파일로 직접 리다이렉트하면 바이트가 보존되지만, stderr와 stdout을 합쳐 파일에 저장할 때는 텍스트로 처리합니다.1 2>&1을 변수로 받거나 PowerShell cmdlet에 전달하면 stdout의 문자열에 stderr에서 만들어진 ErrorRecord 객체가 섞이는 경로가 있습니다. PowerShell 7.6.0 구현도 일반적인 stderr 행을 이 객체로 감쌉니다. 따라서 “7.4 이후에는 합류 결과가 항상 문자열뿐이어서 타입으로 구분할 수 없다”고 단정할 수 없습니다. 확실한 로그를 남기려면 처음부터 stdout과 stderr를 따로 받는 설계가 여전히 명확합니다. 버전뿐 아니라 변수·파이프라인·파일 중 어디서 받는지도 함께 확인하세요(PowerShell에서 외부 exe를 올바르게 호출하기).

쓴 대로 나뉘는지는 손에서 한 번 해 보는 것이 확실합니다. 성공 스트림과 경고 스트림을 둘 다 내는 짧은 코드로 확인할 수 있습니다.

# 경고만 다른 파일로. 화면에는 '데이터'만 남는다
& { Write-Output '데이터'; Write-Warning '주의사항' } 3> warnings.log
Get-Content warnings.log     # 경고 메시지가 들어 있다. '데이터'는 들어가지 않는다

# 모든 스트림을 한 파일로
& { Write-Output '데이터'; Write-Warning '주의사항'; Write-Verbose '상세' -Verbose } *> all.log
Get-Content all.log          # 세 출력이 한데 들어간다

3>를 붙였는데 warnings.log가 비어 있으면, 그 메시지는 경고 스트림에 쓰이지 않은 것입니다(Write-Host로 썼다면 6>입니다). 「로그에 냈을 것이 안 나온다」의 원인 분리는 이 두 줄부터 시작하는 것이 가장 짧습니다.

참고로 *>는 간편하지만, 한데 모아 버리면 나중에 스트림별로 다시 가르기가 어려워집니다. 기계적으로 집계하려면 스트림마다 다른 파일로 내거나, 다음 장의 구조화 로그로 하세요.

실행 증적을 통째로 남기고 싶다면 Start-Transcript가 간편합니다. 세션의 명령과 출력을 텍스트로 기록하므로, 「그때 화면에 무엇이 나왔는지」를 나중에 재현할 수 있습니다.9

Start-Transcript -Path "C:\Logs\export_$(Get-Date -f yyyyMMdd_HHmmss).log" -Append
try   { Invoke-KsExport }
finally { Stop-Transcript }

7. 나중에 분석할 수 있는 로그로 만들기 ── 구조화 로그

사람이 읽는 로그와 기계가 집계하는 로그는 별개입니다. 「지난달 이 에러가 몇 건 나왔는지」를 알고 싶을 때, 자유 형식 텍스트는 grep과의 지구전이 됩니다. 한 줄에 JSON 하나(JSON Lines)로 써 두면 집계는 PowerShell만으로 끝납니다.

function Write-KsLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [ValidateSet('INFO','WARN','ERROR')] [string] $Level,
        [Parameter(Mandatory)] [string] $Message,
        [hashtable] $Data,
        [string] $Path = $script:KsLogPath
    )

    $entry = [ordered]@{
        ts      = (Get-Date).ToString('o')    # ISO 8601. 정렬도 대조도 쉽다
        level   = $Level
        message = $Message
        script  = $MyInvocation.ScriptName
        host    = $env:COMPUTERNAME
        user    = $env:USERNAME
    }
    # 추가 정보는 data 아래에 넣는다. 최상위에 섞으면 호출 측이
    # level이나 user 같은 키를 넘겼을 때 기본 항목을 덮어쓴다
    if ($Data) { $entry['data'] = $Data }

    # -Compress로 한 줄로 만든다. 이어 쓰기는 Add-Content(UTF-8)
    $entry | ConvertTo-Json -Compress -Depth 5 | Add-Content -Path $Path -Encoding utf8

    # 사람용 표시는 표준 체계에 올린다(화면에는 내더라도 값은 반환하지 않는다)
    switch ($Level) {
        # -ErrorAction은 지정하지 않는다. 여기서 고정하면 호출 측이
        # -ErrorAction Stop으로 종료 에러를 만들고 싶어도 못 하게 된다
        'ERROR' { Write-Error   $Message }
        'WARN'  { Write-Warning $Message }
        default { Write-Verbose $Message }
    }
}

# 사용 예
Write-KsLog -Level INFO -Message '가져오기 완료' -Data @{ rows = 1250; file = 'orders.csv'; ms = 4210 }
# → {"ts":"...","level":"INFO","message":"가져오기 완료","script":"...","host":"...","user":"...",
#    "data":{"rows":1250,"file":"orders.csv","ms":4210}}

집계는 이렇게 됩니다.

Get-Content 'C:\Logs\ks.log' |
    ForEach-Object { $_ | ConvertFrom-Json } |
    Where-Object { $_.level -eq 'ERROR' -and [datetime]$_.ts -ge (Get-Date).AddDays(-30) } |
    Group-Object message | Sort-Object Count -Descending | Select-Object Count, Name

Windows 표준 로그 기반(이벤트 로그·ETW)에 올리는 선택지도 있습니다. 감시 도구와의 연동이나 여러 대에서 수집을 생각하면 그쪽이 유리합니다. 설계 비교는 「Windows 이벤트 로그·ETW와 구조화 로그」에, 로그 파일 세대 관리는 「PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화」에 정리했습니다.

8. 진행 표시의 취급

Write-Progress는 호스트의 진행 표시 기능을 쓰는 것으로, 리다이렉트 가능한 데이터 스트림이 아닙니다.5 즉 로그에 남길 수 없습니다. 무인 실행에는 표시할 곳이 없고, 환경에 따라 진행 갱신 비용이 무시할 수 없는 경우도 있습니다.

# 무인 실행 스크립트 앞부분에서 진행 표시를 끈다
$ProgressPreference = 'SilentlyContinue'

진행을 운영 로그에 남기고 싶다면, 마디만 상세 스트림에 쓰는 것이 실용적입니다.

$i = 0
foreach ($row in $rows) {
    $i++
    if ($i % 100 -eq 0) { Write-Verbose "$i / $($rows.Count) 건 완료" }
    ...
}

9. 실무 정석(판단표)

내고 싶은 정보 쓸 것 이유
후속 처리에 넘길 값 Write-Output / 그대로 출력 성공 스트림은 데이터 전용3
처리 경과(조사할 때만 보고 싶다) Write-Verbose 호출 측이 -Verbose로 제어7
운영으로 기록하고 싶은 사건 Write-Information + 구조화 로그 -InformationVariable로 포착 가능2
대화형 도구의 장식 표시 Write-Host 정보 스트림을 거치므로 포착도 가능2
예상 안이지만 주의할 현상 Write-Warning 기본으로 표시되고 -WarningVariable로 포착8
실패 Write-Error / throw 에러 처리는 전용 글을 참고
개발 중 내부 상태 Write-Debug -Debug를 붙였을 때만7
진행 Write-Progress(대화 시에만) 로그에는 안 남는다. 무인 실행에서는 끈다5
실행 증적 통째로 Start-Transcript 자체 로그의 보험으로 병용9

10. 정리

  • PowerShell 출력은 6 스트림으로 나뉩니다. 성공 스트림은 데이터 전용이며, 사람용 메시지를 섞으면 반환값이 깨집니다.
  • PowerShell 5.0 이후의 Write-Host는 정보 스트림에 쓰므로 포착할 수 있지만, 값을 반환하는 용도와 운영 로그 용도에는 쓸 수 없습니다.
  • 함수는 내부에서 출력된 전부를 반환합니다. 불필요한 출력은 $null = ...로 버리는 것이 정석입니다.
  • [CmdletBinding()]을 붙이고 Write-Verbose / Write-Information을 쓰면 상세도 제어를 호출 측에 넘길 수 있습니다. 자체 로그 레벨 변수보다 짧고 의도가 전달됩니다.
  • 나중에 집계할 로그는 한 줄에 JSON 하나인 구조화 로그로 합니다. 화면 재현이 목적이면 Start-Transcript를 같이 씁니다.
  • 진행 표시는 데이터 스트림이 아니므로 로그에 남지 않습니다. 무인 실행에서는 $ProgressPreference = 'SilentlyContinue'로 꺼 버리는 것이 실용적입니다.

샘플 코드 다운로드

이 글에서 다룬 코드는 그대로 돌릴 수 있는 형태로 묶어 배포합니다. 한 줄에 JSON 하나인 구조화 로그와, 반환값을 오염시키지 않는 함수 쓰기가 들어 있습니다.

샘플 코드 다운로드(zip)

이 글의 샘플은 PowerShell 7.6에서 실제로 실행해 검증했습니다(Pester 14건). zip에 들어 있는 Invoke-SampleTests.ps1을 실행하면 손에서도 같은 검증을 재현할 수 있습니다.

# 구문 분석 + 정적 분석 + Pester 테스트
./Invoke-SampleTests.ps1

설정값(경로, 서버 이름, 테넌트 ID 등)은 예입니다. 그대로 프로덕션에서 실행하지 말고, 자사 환경에 맞게 바꿔 읽으세요.

관련 글

관련 상담 영역

合同会社小村ソフト에서는 운영 스크립트 로그 설계 재검토, 「실패한 이유를 모른다」 상태의 해소, 감시·집계로 이어지는 로그 기반 정비를 다룹니다.

참고 링크

  1. Microsoft Learn, about_Redirection. PowerShell이 성공·에러·경고·상세·디버그·정보 각 스트림을 갖고 각각 번호로 식별된다는 것, > >>로 파일에 리다이렉트하는 것, n>&1로 다른 스트림에 합류시키는 것, *>로 모든 스트림을 리다이렉트하는 것에 대해.  2 3 4

  2. Microsoft Learn, Write-Host. PowerShell 5.0 이후 Write-Host가 Write-Information의 래퍼로서 정보 스트림에 출력하게 되어 6> 리다이렉트가 가능해진 것, $InformationPreference 기본 설정 변수와 -InformationAction 공통 매개변수가 Write-Host 메시지에는 영향을 주지 않으며(예외는 -InformationAction Ignore) 그래서 기본으로도 화면에 표시되는 것, -ForegroundColor / -BackgroundColor로 장식하는 것, 출력이 파이프라인에 넘어가지 않는 것에 대해. 관련해 Write-Information은 정보 스트림에 명시적으로 쓰는 것과 -Tags로 분류하는 것에 대해.  2 3 4 5 6 7 8

  3. Microsoft Learn, Write-Output. 객체를 성공 스트림(파이프라인)으로 보내는 것, 명시적으로 부르지 않아도 식의 결과가 같이 출력되는 것에 대해.  2

  4. Microsoft Learn, about_Return. PowerShell 함수가 return 유무와 관계없이 함수 안에서 출력된 모든 객체를 호출 쪽으로 반환하는 것, return은 값을 반환하면서 현재 스코프를 빠져나오기 위한 구문인 것에 대해.  2 3

  5. Microsoft Learn, Write-Progress. 명령 진행 상황을 호스트 진행 표시로 출력하는 것, $ProgressPreference로 표시를 제어할 수 있는 것, PowerShell 7.4 이후는 -ProgressAction 공통 매개변수로도 제어할 수 있는 것에 대해.  2 3 4

  6. Microsoft Learn, about_Functions_CmdletBindingAttribute. [CmdletBinding()] 특성을 붙인 고급 함수가 컴파일된 cmdlet과 같이 동작하고, 공통 매개변수를 자동으로 쓸 수 있게 되는 것에 대해.  2

  7. Microsoft Learn, about_CommonParameters. -Verbose / -Debug / -WarningAction / -InformationAction / -ErrorAction 및 대응하는 -*Variable 매개변수의 동작, 기본 설정 변수와의 관계에 대해.  2 3 4 5

  8. Microsoft Learn, about_Preference_Variables. $VerbosePreference·$DebugPreference·$InformationPreference의 기본값이 SilentlyContinue, $WarningPreference와 $ErrorActionPreference의 기본값이 Continue인 것, $ProgressPreference로 진행 표시를 제어하는 것, 이들이 스코프와 자식 스코프에 적용되는 것에 대해.  2 3

  9. Microsoft Learn, Start-Transcript. 세션의 명령과 콘솔 출력을 텍스트 파일에 기록하는 것, -Append로 이어 쓰기, Stop-Transcript로 중지하는 것에 대해.  2 3

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

Write-Host는 쓰면 안 되나요?
금지가 아니라 용도가 한정된다는 이해가 정확합니다. PowerShell 5.0 이후의 Write-Host는 정보 스트림(6번)에 쓰기 때문에, 6>로 리다이렉트하거나 -InformationVariable로 잡을 수 있게 되었습니다. 다만 기본적으로 항상 화면에 나온다는 전제의 명령이라, 값을 파이프라인에 흘리고 싶을 때는 쓸 수 없습니다. 대화형 도구에서 사람이 읽도록 꾸민 표시라면 Write-Host, 처리 진행이나 보충 정보라면 Write-Verbose나 Write-Information, 후속 처리에 넘길 값이라면 Write-Output(또는 그대로 출력)으로 나눕니다.
함수 반환값에 의도하지 않은 값이 섞입니다.
PowerShell 함수는 명시적인 return이 없어도, 함수 안에서 출력된 모든 객체를 반환하기 때문입니다. New-Item이나 StringBuilder의 .Append()처럼 값을 반환하는 명령이나 메서드를 그냥 호출만 하면, 그 반환값이 성공 스트림으로 흘러 호출 쪽에 도착합니다. 반대로 List[T]의 .Add()처럼 반환값이 void인 메서드는 아무것도 출력하지 않으므로 억제할 필요가 없습니다. 대처는 불필요한 출력을 $null = ...로 버리거나, | Out-Null을 붙이거나, [void]로 캐스트하는 것입니다. 성능 면에서는 $null = ...가 가장 가벼운 방식입니다.
스크립트의 상세 로그를 실행 시점에 켜고 끄고 싶습니다.
처리 중간 경과를 Write-Verbose로 쓰고, 함수에 [CmdletBinding()]을 붙이세요. 그것만으로 호출 측이 -Verbose를 지정했을 때만 표시됩니다. 항상 내고 싶다면 $VerbosePreference = 'Continue'를 스크립트 앞부분에서 설정합니다. 마찬가지로 Write-Debug는 -Debug, Write-Warning은 -WarningAction으로 호출 측에서 제어할 수 있습니다. 자체 로그 레벨 변수를 만들기보다 PowerShell 표준 체계에 올리는 편이, 다른 사람이 읽었을 때 의도가 전달됩니다.
상세 로그와 경고까지 통째로 파일에 남기려면 어떻게 하나요?
용도에 따라 세 가지입니다. 단순히 모든 스트림을 파일로 떨어뜨리려면 *>로 리다이렉트합니다. 실행 증적으로 화면에 나온 것을 통째로 남기려면 Start-Transcript가 간편합니다. 프로그램에서 나중에 분석하려면 한 줄에 JSON 하나인 구조화 로그를 자체 로그 함수로 쓰는 것이 확실하고, 이 경우에도 보험으로 트랜스크립트를 같이 쓸 가치가 있습니다.
Write-Progress 표시는 로그 파일에 남길 수 있나요?
남길 수 없습니다. 진행 표시는 호스트의 표시 기능이며, 리다이렉트 가능한 데이터 스트림과는 별개로 취급되기 때문입니다. 무인 실행에는 표시할 곳이 없으므로, 진행 상황은 로그에 남길 정보와 나눠 생각하세요. 무인 실행에서는 $ProgressPreference = 'SilentlyContinue'로 진행 표시 자체를 끄면, 환경에 따라 눈에 띄게 빨라지기도 합니다. 진행을 로그에 남기고 싶다면 Write-Verbose로 「100건 중 50건 완료」처럼 마디만 쓰는 편이 실용적입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기