PowerShell로 Excel・CSV 업무 처리를 자동화한다 ── 집계・대조・장표 출력의 실무 레시피

· 업데이트: · · PowerShell, Windows, CSV, Excel, 자동화, 업무 효율화, 스크립트, 기존 자산 활용

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

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

Codex 리뷰에 따라 상담·문의 링크에 /ko/ 로케일 접두를 붙였습니다. 본문의 기술적인 주장은 바꾸지 않았습니다.
기사 맨 앞에 「이 글의 지식 맵」 절을 추가했습니다. 본문에서 다루는 개념과 그 관계를 요약·그림·상세 페이지 링크로 정리한 것입니다. 본문의 주장은 바꾸지 않았습니다.
외부 리뷰(1283건)에 대응해 본문을 갱신했습니다. 개별 변경 내용은 아래 이력을 참고하세요.
입력 샘플 CSV와, 그 코드가 내는 집계 결과를 표로 추가했습니다. 맨 앞에 전제 지식을 두고, 다른 글로 넘겼던 CSV 함정의 요점(Excel로 확인하지 않기, 따옴표 처리는 `Export-Csv`에 맡기기, `=`로 시작하는 값, 줄바꿈 코드)을 본문에 요약했습니다. ImportExcel 도입에 TLS 1.2 주의사항을 더하고, `Compare-Object`의 `$null` 대책을 실제 코드에 반영했습니다.
최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22174618)

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

Go Komura (2026). 「PowerShell로 Excel・CSV 업무 처리를 자동화한다 ── 집계・대조・장표 출력의 실무 레시피」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/powershell-excel-csv-automation-recipes/

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

「매달, 기간계 시스템에서 내려받은 CSV를 Excel로 열어 피벗으로 집계하고, 서식을 다듬어 메일로 보낸다」「두 시스템에서 나온 명부를 맞춰 보고 차이를 눈으로 찾는다」── 이런 정형 작업 상담을 자주 받습니다. 한 번에 30분이라도, 매주·매월·여러 사람이 반복하면 연간으로는 상당한 시간이 사라지고, 수작업 복사·붙여넣기에는 반드시 실수가 섞입니다.

PowerShell은 이 영역과 잘 맞는 도구입니다. Windows에 기본으로 들어 있고(Windows PowerShell 5.1), CSV를 「객체의 표」로 읽어 집계·대조·출력까지 파이프라인으로 이을 수 있습니다. 커뮤니티 제작 ImportExcel 모듈을 쓰면 Excel 본체 없이 xlsx 장표까지 만들 수 있습니다.

다만 이 분야에는 일본어 환경 특유의 함정이 있습니다. 문자 코드 기본값이 Windows PowerShell 5.1과 PowerShell 7에서 완전히 달라, 「내 PC에서는 됐는데 다른 환경에서는 글자가 깨졌다」가 자주 일어납니다. 이 글에서는 중소기업 정보시스템·업무 담당자가 CSV·Excel 업무를 PowerShell로 자동화할 때의 실무 레시피를, 문자 코드 함정부터 순서대로 정리합니다.

전제 지식: 대상은 중소기업 정보시스템·업무 담당자이지만, PowerShell 입문 글은 아닙니다. 변수와 파이프라인, foreachif 작성법, 스크립트 파일(.ps1)을 만들어 실행하는 절차까지는 알고 있다고 보고 진행합니다. 이 부분이 불안하다면 「PowerShell 명령의 기본 ── 먼저 익힐 조작과 안전한 사용법」을 먼저 읽어 주세요. 해시테이블(@{}), try/finally, COM 참조 해제 같은 중급 이상 작성법도 나오지만, 각각 등장하는 곳에서 「왜 그렇게 쓰는지」를 붙입니다. 실행 환경은 Windows PowerShell 5.1과 PowerShell 7 양쪽을 전제로 하고, 동작이 다른 부분은 그때마다 구분해 적습니다.

1. 먼저 결론

  • CSV 읽기·쓰기에서는 -Encoding을 항상 명시합니다. Windows PowerShell 5.1은 cmdlet마다 기본 인코딩이 제각각이고, PowerShell 7은 일괄 BOM 없는 UTF-8이라, 둘의 기본값이 완전히 다르기 때문입니다.1
  • 5.1의 Export-Csv 기본값은 ASCII입니다. -Encoding을 빠뜨리면 저장 시점에 일본어가 사라집니다. 또한 5.1의 Import-Csv는 BOM 없는 파일을 UTF-8로 해석하므로, Shift_JIS CSV를 그대로 읽으면 글자가 깨집니다.1
  • 집계는 Group-Object(그룹화)와 Measure-Object(합계·평균·최댓값·최솟값)의 조합이 기본형입니다. Excel 피벗으로 매번 하던 집계의 상당수는 이 둘로 바꿀 수 있습니다.23
  • 두 CSV의 대조는, 차이 유무만 볼 때는 Compare-Object, 열을 끌어와 결합까지 필요하면 해시테이블입니다. Compare-Object는 SideIndicator로 어느 쪽에만 있는 행인지를 보여 줍니다.4
  • xlsx 읽기·쓰기는 ImportExcel 모듈(커뮤니티 제작)이 1순위입니다. Excel 본체 설치는 필요 없고, 테이블화·서식·피벗 테이블까지 만들 수 있습니다.5
  • Excel 본체의 COM 조작은 최후 수단입니다. 참조(RCW) 해제 누락으로 EXCEL.EXE가 남기 쉽고67, 애초에 Microsoft는 무인 환경(서비스나 예약 실행)에서의 Office 자동화를 권장·지원하지 않습니다.8
  • 작업 스케줄러에 올리기 전에 인코딩·경로·실행 환경, 이 세 가지를 명시적으로 고정합니다. 대화형 실행에서는 되던 스크립트가 정기 실행에서 깨지는 원인 대부분은 이 세 가지입니다.18

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

2. Import-Csv/Export-Csv의 기본 ── 가장 큰 함정은 문자 코드

Import-Csv는 CSV를 「1행=1객체, 1열=1속성」인 표로 읽습니다. 헤더 행이 열 이름이 되고, 이후 처리는 모두 속성 이름으로 작성합니다.9 Export-Csv는 그 반대로, 객체의 열을 CSV로 씁니다.10 여기까지는 간단합니다. 문제는 문자 코드입니다.

공식 문서에 명시된 대로, Windows PowerShell 5.1의 기본 인코딩은 cmdlet마다 일관되지 않습니다.1 일본어 환경 실무에 영향을 주는 범위를 표로 정리합니다.

조작 Windows PowerShell 5.1의 기본값 PowerShell 7의 기본값
Export-Csv ASCII(일본어는 사라짐)1 BOM 없는 UTF-810
Import-Csv(BOM 없는 파일) UTF-8로 해석1 BOM 없는 UTF-8
Get-Content(BOM 없는 파일) ANSI=일본어 환경에서는 Shift_JIS1 BOM 없는 UTF-8
Out-File·리다이렉트(>) UTF-16LE(BOM 있음)1 BOM 없는 UTF-8

즉 5.1에서는 「Get-Content로 읽으면 Shift_JIS로 읽혔는데 Import-Csv로는 깨진다」「Export-Csv했더니 일본어가 전부 ?가 됐다」가 모두 사양대로 일어납니다. PowerShell 7은 일괄 BOM 없는 UTF-8로 일관되지만1, 이번에는 Shift_JIS로 도착하는 기간계 시스템의 CSV를 기본값 그대로 읽으면 깨집니다. 결론은 하나입니다. 읽기·쓰기 양쪽에서 -Encoding을 명시합니다.

# 기간계 시스템이 출력하는 Shift_JIS CSV를 읽는다
# Windows PowerShell 5.1: Default = 시스템의 ANSI 코드 페이지(일본어 환경에서는 Shift_JIS)
$orders = Import-Csv -LiteralPath 'C:\data\orders.csv' -Encoding Default

# PowerShell 7에서는 코드 페이지 번호로 지정할 수 있다(932 = Shift_JIS)
# $orders = Import-Csv -LiteralPath 'C:\data\orders.csv' -Encoding 932

# 출력은 BOM 있는 UTF-8로 두면, 더블클릭으로 Excel에 열어도 잘 깨지지 않는다
# PowerShell 7: UTF8은 BOM 없음이 되므로, BOM 있는 쪽은 utf8BOM으로 명시한다
$orders | Export-Csv -LiteralPath 'C:\data\orders_out.csv' -NoTypeInformation -Encoding utf8BOM
# Windows PowerShell 5.1: utf8BOM이라는 값은 없다. UTF8을 지정하면 BOM이 붙는다
# $orders | Export-Csv -LiteralPath 'C:\data\orders_out.csv' -NoTypeInformation -Encoding UTF8

PowerShell 6.2 이후의 -Encoding은 코드 페이지 번호(932)나 등록 이름으로도 지정할 수 있고, 7.4 이후에는 ansi 값도 쓸 수 있습니다.1 -NoTypeInformation은 5.1에서 맨 앞에 붙는 #TYPE 행을 막기 위한 것으로, PowerShell 6 이후에는 기본으로 붙지 않아 지정이 필요 없습니다(붙여도 오류는 나지 않습니다).10 5.1과 7 양쪽에서 돌릴 스크립트에서는 붙여 두는 편이 무난합니다.

CSV 형식 자체의 함정(Excel로 열면 앞자리 0이 사라짐, 쉼표나 줄바꿈을 포함한 값, 인젝션 대책)은 「CSV는 「그냥 텍스트」가 아니다」에서 자세히 다룹니다. 문자 코드와 줄바꿈 코드의 기초는 「Windows의 문자 코드와 줄바꿈 코드」를 참고하세요.

이 글만으로 판단할 수 있도록, 위임한 글의 요점을 세 줄만 먼저 적습니다. 확인하려고 CSV를 Excel에서 더블클릭하지 마세요(앞자리 0이 빠지거나 긴 숫자가 지수 표기가 되어, 확인했다고 생각한 값이 실제로는 깨진 값입니다. 내용을 볼 때는 텍스트 에디터나 Import-Csv로). 쉼표·줄바꿈·큰따옴표를 포함한 값은 따옴표로 감싸야 합니다(Export-Csv가 이 처리를 자동으로 하므로, 직접 문자열을 이어 붙여 CSV를 만들지 마세요). =로 시작하는 값은 스프레드시트 쪽에서 수식으로 해석될 수 있습니다(외부에서 받은 CSV를 함부로 열지 마세요). 줄바꿈 코드는 Windows 이외나 오래된 기기에서 온 파일에서 LF인 경우가 있고, Import-Csv는 문제없이 읽지만, 직접 -split하는 처리를 작성할 때는 CRLF를 전제로 하지 마세요.

3. 집계 ── Group-Object와 Measure-Object로 피벗에 해당하는 처리를 만든다

「부서별 건수와 금액 합계」 같은 집계는 Group-Object로 그룹화하고, 각 그룹을 Measure-Object로 합산하는 것이 기본형입니다.23

먼저 입력을 정해 둡니다. 아래 레시피는 이런 orders.csv(기간계 시스템 출력을 가정. 문자 코드는 Shift_JIS)를 전제로 합니다.

OrderNo,Date,Dept,Customer,Amount
1001,2026/07/01,営業1課,株式会社A,120000
1002,2026/07/01,営業2課,株式会社B,80000
1003,2026/07/02,営業1課,株式会社C,45000
1004,2026/07/03,管理部,株式会社A,15000
1005,2026/07/03,営業2課,株式会社D,230000
# Shift_JIS CSV를 PowerShell 7에서 읽으므로 코드 페이지 932를 명시한다
# (7의 -Encoding Default는 UTF-8 의미가 되어 깨진다. 5.1에서 돌릴 때는 Default를 지정)
$orders = Import-Csv -LiteralPath 'C:\data\orders.csv' -Encoding 932

# 부서별로 건수와 금액 합계를 낸다.
# Import-Csv의 값은 모두 문자열이므로, [decimal]로 변환한 뒤 합산하는 것이 핵심
# (Measure-Object -Sum의 Sum은 Double형이 되므로, 금액은 decimal 그대로 직접 더해
#  자릿수가 큰 합계나 소수 정밀도를 떨어뜨리지 않는다)
$summary = $orders | Group-Object -Property Dept | ForEach-Object {
    $total = [decimal]0
    foreach ($row in $_.Group) { $total += [decimal]$row.Amount }
    [pscustomobject]@{
        Dept  = $_.Name    # 그룹화 키의 값
        Count = $_.Count   # 건수
        Total = $total     # 금액 합계
    }
}

$summary | Sort-Object -Property Total -Descending |
    Export-Csv -LiteralPath 'C:\data\summary.csv' -NoTypeInformation -Encoding UTF8

위 5행에 대해 $summary(정렬 후)가 가진 값은 다음과 같습니다. summary.csv에는 이 3행이 헤더와 함께 쓰입니다.

Dept Count Total
営業2課 2 310000
営業1課 2 165000
管理部 1 15000

따라 쳤다면, 먼저 이 3행이 나오는지 확인하세요. 금액이 안 맞거나 건수가 안 맞으면, 부서명 표기 흔들림(전각 공백이나 「営業1課」)이거나, 다음에 적을 형 변환 문제입니다.

빠지기 쉬운 지점은 CSV 값이 모두 문자열이라는 것입니다. Import-Csv가 반환하는 것은 문자열 속성의 모임이므로9, 숫자로 생각하고 Measure-Object -Sum에 넘기기 전에 [decimal]로 명시적으로 변환합니다. 변환을 잊어도 5.1에서는 그럭저럭 돌아가는 경우가 있어, 금액 자릿수가 어긋난 뒤에야 알아채는 사고로 이어지기 쉽습니다.

Measure-Object-Sum 외에 -Average -Maximum -Minimum도 동시에 구할 수 있습니다.3 Group-Object -AsHashTable을 쓰면 「키→그 그룹의 행 배열」인 해시테이블을 바로 얻을 수 있고, 뒤에서 다룰 대조에도 응용할 수 있습니다.2 이런 단발 명령의 쓰임새는 「PowerShell 실용 명령 모음」에 정리해 두었습니다.

4. 대조 ── Compare-Object와 해시테이블 결합을 가려 쓰기

4.1. 차이 유무만 볼 때는 Compare-Object

어제와 오늘의 명부 CSV에서 「늘어난 사람·사라진 사람」을 뽑는 전형적인 대조는 Compare-Object가 가장 짧습니다. -Property로 키 열을 지정하면 그 열 값만으로 비교하고, 결과 SideIndicator가 =>(비교 대상 쪽에만 있음)인지 <=(기준 쪽에만 있음)인지로 증감을 알 수 있습니다.4

# @()로 감싸는 것은, 0건이나 빈 파일일 때 Import-Csv 결과가 $null이 될 수 있기 때문.
# ReferenceObject/DifferenceObject가 $null이면 Compare-Object는 종료 오류로 멈춘다
$yesterday = @(Import-Csv -LiteralPath '.\users_0716.csv' -Encoding UTF8)
$today     = @(Import-Csv -LiteralPath '.\users_0717.csv' -Encoding UTF8)

# 사원 번호만으로 비교한다. => 는 오늘만 있는 행(추가), <= 는 어제만 있는 행(삭제)
Compare-Object -ReferenceObject $yesterday -DifferenceObject $today -Property EmpNo |
    Sort-Object -Property EmpNo |
    Format-Table -Property EmpNo, SideIndicator

주의점은 두 가지입니다. 첫째, -Property를 지정하면 결과에는 그 열과 SideIndicator만 남으므로, 이름 등 다른 열도 보려면 결과 키로 원본 데이터를 다시 조회해야 합니다. 둘째, 기준 쪽·비교 대상 쪽 중 하나가 $null(0행이 아니라 null)이면 오류로 멈춥니다.4 위 코드에서 읽기 결과를 @()로 감싼 것은 이 대책입니다. 0건 CSV라도 빈 배열이 되므로, 「오늘은 전원이 퇴사했다(=모든 행이 <=로 나옴)」 같은 극단적인 날에도 형식 오류가 아니라 차이로 결과를 받을 수 있습니다.

4.2. 열을 끌어와 결합까지 필요하면 해시테이블

「명세 CSV의 사원 번호에, 마스터 CSV에서 이름과 부서를 끌어온다」는 SQL JOIN에 해당하는 작업은, 마스터 쪽을 키→행 해시테이블로 만든 뒤 한 행씩 조회하는 것이 정석입니다. 이중 루프(명세×마스터 전수 비교)는 수천 건×수천 건에서 눈에 띄게 느려지지만, 해시테이블이면 수만 건이라도 실무 속도로 돌아갑니다.

입력은 다음 두 파일입니다.

master.csv
EmpNo,Name,Dept
E001,山田 太郎,営業1課
E002,佐藤 花子,管理部

details.csv
EmpNo,Amount
E001,120000
E003,45000
# 마스터 쪽을 「사원 번호 → 행」 해시테이블로 만든다
# 키가 중복되면 나중 행으로 덮어쓰므로, 중복이 있을 수 있으면 사전에 점검한다
$master = @{}
foreach ($row in (Import-Csv -LiteralPath '.\master.csv' -Encoding UTF8)) {
    $master[$row.EmpNo] = $row
}

# 명세를 한 행씩 끌어온다. 찾지 못한 행은 버리지 않고 별도 파일로 뺀다
$unmatched = New-Object System.Collections.Generic.List[object]
$joined = foreach ($row in (Import-Csv -LiteralPath '.\details.csv' -Encoding UTF8)) {
    $hit = $master[$row.EmpNo]
    if ($null -eq $hit) {
        $unmatched.Add($row)
        continue
    }
    [pscustomobject]@{
        EmpNo  = $row.EmpNo
        Name   = $hit.Name
        Dept   = $hit.Dept
        Amount = $row.Amount
    }
}

$joined    | Export-Csv -LiteralPath '.\joined.csv'    -NoTypeInformation -Encoding UTF8
$unmatched | Export-Csv -LiteralPath '.\unmatched.csv' -NoTypeInformation -Encoding UTF8

이 입력이면 joined.csv에는 마스터에 있던 E001 1행(E001,山田 太郎,営業1課,120000)이, unmatched.csv에는 마스터에 없던 E003 1행(E003,45000)이 나옵니다.

이 레시피의 핵심은 마지막 두 줄입니다. 키를 찾지 못한 행을 조용히 버리지 않습니다. 대조 업무의 가치는 「맞지 않은 것을 사람이 확인할 수 있다」는 데 있으므로, unmatched를 반드시 출력하고 건수를 로그나 표준 출력에 남깁니다. 위 예라면, unmatched.csvE003이 나온 것을 알아챌 수 있는지가 이 처리를 돌리는 의미 그 자체입니다.

4.3. 열 이름 흔들림과 헤더 처리

현장 CSV는 「사원번호」「사원No」「emp_no」처럼 열 이름이 흔들리기 쉽습니다. 대처는 읽은 직후 내부 이름으로 정규화하는 것으로 끝입니다. 이후 처리를 내부 이름만으로 작성하면, 서식이 바뀌어도 고칠 곳은 정규화 한 곳입니다.

# 헤더 행이 없는 CSV는 -Header로 열 이름을 준다(1행째부터 데이터로 읽힌다)
# 문자 코드는 2장과 같다: Shift_JIS라면 7에서는 932, 5.1에서는 Default를 지정한다
$rows = Import-Csv -LiteralPath '.\no_header.csv' -Header 'EmpNo', 'Name', 'Dept' -Encoding 932

# 일본어 헤더 CSV는, 읽은 직후 영문 내부 이름으로 정규화한다
$normalized = Import-Csv -LiteralPath '.\jinji.csv' -Encoding 932 |
    Select-Object -Property @{ Name = 'EmpNo'; Expression = { $_.'社員番号' } },
                            @{ Name = 'Name';  Expression = { $_.'氏名' } },
                            @{ Name = 'Dept';  Expression = { $_.'所属部署' } }

-Header는 헤더 행이 없는 파일용이며, 지정하면 1행째도 데이터로 읽힌다는 점에 주의하세요.9 헤더에 빈칸이 있으면 PowerShell이 H1 같은 임시 열 이름을 자동으로 붙이므로9, 기대한 열 이름으로 속성을 꺼낼 수 없을 때는 먼저 헤더 행을 의심합니다.

5. xlsx를 다루기 ── ImportExcel이 1순위, COM은 최후 수단

5.1. ImportExcel 모듈 ── Excel 본체 없이 xlsx를 읽고 쓴다

집계 결과를 「CSV가 아니라 Excel 파일로, 표에 색을 입혀」 달라고 하는 것이 일본 직장입니다. 여기서 1순위가 되는 것은 PowerShell Gallery에 공개된 커뮤니티 제작 ImportExcel 모듈입니다. Excel 본체 설치 없이 xlsx를 읽고 쓸 수 있고, 테이블화·열 너비 조정·피벗 테이블까지 만들 수 있습니다.5

# 최초 1회만. PowerShell Gallery에서 현재 사용자 범위로 설치한다(관리자 권한은 필요 없다)
# Windows PowerShell 5.1에서 Gallery에 연결하지 못하면, 먼저 TLS 1.2를 켠 뒤 실행한다
# [Net.ServicePointManager]::SecurityProtocol =
#     [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
Install-Module -Name ImportExcel -Scope CurrentUser

# 설치 여부와 버전을 확인한다
Get-InstalledModule -Name ImportExcel | Select-Object -Property Name, Version

# xlsx를 읽는다(Import-Csv와 같은 감각으로, 시트의 표가 객체 배열이 된다)
$budget = Import-Excel -Path 'C:\data\budget.xlsx' -WorksheetName '予算'

# 3장의 집계 결과를, 테이블+열 너비 자동 조정+피벗 테이블이 붙은 xlsx로 출력한다
$summary | Export-Excel -Path 'C:\data\monthly-report.xlsx' `
    -WorksheetName '集計' -TableName 'Summary' -AutoSize `
    -IncludePivotTable -PivotRows Dept -PivotData @{ Total = 'Sum' }

도입에서 막히기 쉬운 점은 두 가지입니다. 하나는 PowerShell Gallery 연결로, Gallery 이용에는 TLS 1.2 이상이 필요하므로 Windows PowerShell 5.1에서는 위 주석처럼 세션에서 명시하지 않으면 Install-Module이 실패하는 경우가 있습니다(매번 쓰기 번거롭다면 이 한 줄을 프로필 스크립트에 넣어둡니다).11 다른 하나는 버전 확인으로, 이 모듈이 대응하는 PowerShell 버전은 배포 페이지에 명시되어 있지 않습니다.5 5.1과 7이 섞인 사내에서 쓴다면, 실제로 운영할 쪽 PowerShell에서 Export-Excel을 한 번 돌려 파일이 열리는지 확인한 뒤 배포하세요(작업 스케줄러에서 돌릴 거면, 작업이 기동하는 것과 같은 실행 계정으로 확인합니다. -Scope CurrentUser로 넣은 모듈은 그 사용자에게만 보입니다).

커뮤니티 제작인 이상, 도입 시에는 사내 소프트웨어 도입 규칙에 따라 확인하세요(모듈 입수처는 PowerShell Gallery입니다5). 그래도 「서버에 Excel을 설치하고 COM으로 돌리는」 구성에 비하면, 라이선스·안정성·유지보수 어느 면에서든 나은 선택입니다. 장표 생성 방식 비교(COM/Open XML/템플릿)는 「Excel 장표 출력 만드는 법」에서 정리합니다.

5.2. Excel COM 조작 ── 쓸 거면 해제까지 다 작성하고, 무인 실행은 하지 않는다

기존 xls 매크로를 돌리고 싶다, Excel 기능 자체(재계산, 인쇄, 이름 정의 해석 등)가 필요하다── 그런 경우에만 Excel 본체를 COM으로 조작합니다. PowerShell에서는 New-Object -ComObject로 기동할 수 있지만, 유명한 문제가 EXCEL.EXE 프로세스 잔류입니다. COM 객체 참조(RCW)가 해제되지 않으면 Quit()을 호출해도 프로세스가 남습니다.6 .NET 쪽 대처는 Marshal.ReleaseComObject로 명시적으로 해제하는 것입니다.7

# COM은 최후 수단. 「연 것은 반드시 닫고, 참조는 반드시 해제한다」를 finally로 보장한다
# Excel 기동 후의 COM 호출은 모두 try 안에서 한다(기동 직후 1행에서 예외가 나도 뒷정리가 돌도록)
$excel = New-Object -ComObject Excel.Application
$book = $null
try {
    $excel.Visible = $false
    $excel.DisplayAlerts = $false   # 확인 대화 상자에서 멈추지 않게 한다
    $book  = $excel.Workbooks.Open('C:\data\template.xlsx')
    $sheet = $book.Worksheets.Item(1)
    $sheet.Range('B2').Value2 = 12345
    $book.SaveAs('C:\data\output.xlsx')
    $book.Close($false)
}
finally {
    # Quit이 실패해도(Excel이 응답 없음 등) 해제와 GC는 반드시 실행한다
    try {
        $excel.Quit()
    }
    finally {
        # 다룬 COM 객체는 RCW를 명시 해제하지 않으면 EXCEL.EXE가 남기 쉽다
        if ($sheet) { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($sheet) }
        if ($book)  { [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($book) }
        [void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($excel)
        [GC]::Collect()
        [GC]::WaitForPendingFinalizers()
    }
}

까다로운 점은 $excel.Workbooks.Open(...)처럼 점을 잇기만 해도 중간 객체(이 예에서는 Workbooks 컬렉션) 참조가 생기고, 그것도 해제 대상이라는 것입니다. 위 코드에서도 엄밀히는 Workbooks 참조가 남아 있어, 확실히 하려면 중간 객체도 변수로 받아 해제합니다. 이 「참조 관리를 끝까지 작성하기 어렵다」는 점 자체가 COM을 최후 수단으로 두는 이유입니다. 구조 세부와 대체 판단은 「C# Excel 조작에서 EXCEL.EXE가 남는 문제」에서, PowerShell에서의 COM/.NET 호출 전반은 동시에 공개한 「PowerShell에서 COM과 .NET을 호출하는 실전」에서 다룹니다.

그리고 또 하나 결정적인 제약이 있습니다. Microsoft는 무인·비대화형 클라이언트(서비스, 예약 실행 등)에서의 Office 자동화를 권장하지 않으며, 지원도 하지 않습니다. Office는 대화형 사용자가 있다는 전제로 설계되어, 무인 환경에서는 불안정한 동작이나 데드락이 일어날 수 있다고 공식으로 명시되어 있습니다.8 SSIS 문서에서도 무인 실행 환경에서는 Excel 연결을 쓰지 말고 CSV나 Open XML 계열 방식으로 바꾸라고 권장합니다.12 「매일 밤 작업 스케줄러로 Excel을 열어 장표를 만드는」 구성은, 되는 것처럼 보여도 사상누각입니다. 정기 실행하고 싶은 처리는 ImportExcel이나 Open XML 계열로 옮기세요.

6. 작업 스케줄러로 정기 실행하기 전 체크리스트

손에서 되던 스크립트를 그대로 작업 스케줄러에 올리면, 대개 다음 중 하나로 깨집니다.

  • 문자 코드: 대화형 실행과 예약 실행에서 동작 자체는 바뀌지 않지만, 「우연히 로컬의 7에서 되던」 스크립트가 작업 쪽 시작 명령이 powershell.exe(=5.1)여서 기본 인코딩이 바뀌어 깨지는 일은 흔한 사고입니다. 2장대로 읽기·쓰기 양쪽에서 -Encoding을 명시하면, 어느 쪽으로 기동되어도 결과는 같아집니다.1
  • 경로: 현재 디렉터리에 의존한 상대 경로는 쓰지 말고, 절대 경로나 $PSScriptRoot 기준으로 작성합니다. 네트워크 드라이브(Z: 등)는 예약 실행 세션에서 보이지 않으므로 UNC 경로로 합니다.
  • 실행 환경: 어느 PowerShell로 돌릴지(powershell.exe인지 pwsh.exe인지)를 시작 명령에서 명시합니다. 5.1과 7의 차이와 가려 쓰기는 동시에 공개한 「Windows PowerShell 5.1과 PowerShell 7의 차이」를 참고하세요.
  • Excel COM을 포함한 처리는 올리지 않습니다: 앞 장대로 무인 실행은 비지원입니다.8

작업 스케줄러 실행 시 로그·증적을 남기는 방법은 「PowerShell 스크립트 응용 ── 로그 조사·아카이브·리포트화를 안전하게 자동화하기」에서 자세히 다룹니다.

7. 실무 정석(판단표)

논점 선택지 판단 기준
CSV의 문자 코드 기본값에 맡긴다 / -Encoding 명시 항상 명시. 5.1과 7에서 기본값이 달라 「환경이 바뀌면 깨진다」를 구조적으로 막음1
집계 Excel 수작업 / Group-Object+Measure-Object 매주·매월 반복하면 스크립트화. 절차가 코드로 남아 재현 가능해짐23
두 CSV의 대조 Compare-Object / 해시테이블 결합 차이 유무만이면 Compare-Object. 다른 열을 끌어오면 해시테이블. 불일치 행은 반드시 별도 출력4
xlsx 출력 CSV로 끝낸다 / ImportExcel / COM 서식·피벗이 필요하면 ImportExcel(Excel 본체 불필요). COM은 매크로 실행 등 Excel 기능 자체가 필요할 때만5
Excel COM 실행 형태 작업 스케줄러 무인 실행 / 수동·대화형 실행만 무인 Office 자동화는 비지원. 무인화할 처리는 Excel 본체 불필요 방식으로 옮김8
정기 실행 손에서 그때그때 실행 / 작업 스케줄러 -Encoding·절대 경로·기동할 PowerShell, 이 세 가지를 명시한 뒤 작업화

8. 정리

  • CSV 자동화의 가장 큰 함정은 문자 코드입니다. 5.1은 기본값이 cmdlet마다 제각각(Export-Csv는 ASCII), 7은 일괄 BOM 없는 UTF-8입니다. 읽기·쓰기 양쪽에서 -Encoding을 명시하면 환경 차이를 없앨 수 있습니다.
  • 집계는 Group-Object+Measure-Object가 기본형입니다. CSV 값은 전부 문자열이므로, 숫자로 변환한 뒤 집계합니다.
  • 대조는 차이 유무만이면 Compare-Object, 결합이 필요하면 해시테이블입니다. 끌어오지 못한 행은 반드시 별도 파일로 내어 사람이 확인할 수 있게 합니다.
  • 열 이름 흔들림은 읽은 직후 정규화로 흡수하고, 이후는 내부 이름만으로 처리를 작성합니다.
  • xlsx는 ImportExcel 모듈로 Excel 본체 없이 읽고 쓸 수 있습니다. COM은 해제 처리까지 다 작성할 수 있는 경우의 최후 수단이며, 무인 실행에는 쓰지 않습니다.
  • 작업 스케줄러에 올리기 전에 인코딩·경로·기동할 PowerShell을 명시적으로 고정하세요.

관련 글

관련 상담 영역

합동회사 코무라소프트에서는 CSV·Excel을 거친 정형 업무 자동화 스크립트 작성, Excel COM 의존 장표 처리에서의 탈피(ImportExcel/Open XML로 교체), 「특정 환경에서만 글자가 깨진다」「EXCEL.EXE가 남는다」 같은 장애 조사를 다룹니다.

참고 링크

  1. Microsoft Learn, about_Character_Encoding. Windows PowerShell 5.1의 기본 인코딩이 cmdlet마다 일관되지 않는다는 점(Export-Csv는 ASCII, Set-Content/Get-Content는 ANSI인 Default, Out-File과 리다이렉트는 UTF-16LE, BOM 없는 파일의 Import-Csv는 UTF-8 해석), PowerShell 6 이후가 일괄 BOM 없는 UTF-8을 기본으로 한다는 점, 6.2 이후 코드 페이지 번호·등록 이름으로 -Encoding 지정이 가능하다는 점, 7.4의 ansi 값에 대해.  2 3 4 5 6 7 8 9 10 11 12

  2. Microsoft Learn, Group-Object. Group-Object가 지정 속성 값으로 객체를 그룹화하고, 그룹별 건수와 요소를 반환한다는 점, -AsHashTable로 키→그룹 해시테이블을 얻을 수 있다는 점에 대해.  2 3 4

  3. Microsoft Learn, Measure-Object. Measure-Object로 건수 외에 Sum·Average·Maximum·Minimum(및 표준편차)을 계산할 수 있다는 점에 대해.  2 3 4

  4. Microsoft Learn, Compare-Object. Compare-Object가 두 객체 집합을 비교하고, SideIndicator(<= / => / ==)로 어느 쪽에만 존재하는지를 나타낸다는 점, -Property로 지정한 열만 비교할 수 있다는 점, 기준 쪽·비교 대상 쪽이 null이면 종료 오류가 된다는 점에 대해.  2 3 4

  5. Microsoft Learn, An active Excel process continues to run after using a VBA macro to programmatically quit Excel. 통합 문서를 닫고 Quit을 호출해 참조를 지워도, Excel이나 그 멤버에 대한 참조가 전역으로 유지되면 EXCEL.EXE 프로세스가 계속 남는다는 점에 대해.  2

  6. Microsoft Learn, Marshal.ReleaseComObject(Object) Method. ReleaseComObject가 COM 객체에 연결된 RCW(런타임 호출 가능 래퍼)의 참조 카운트를 줄여 COM 객체 수명을 명시적으로 제어하는 데 쓴다는 점, 해제된 객체에 접근하면 예외가 되므로 사용에 주의가 필요하다는 점에 대해.  2

  7. Microsoft Learn, Considerations for unattended automation of Office in the Microsoft 365 for unattended RPA environment. Microsoft가 무인·비대화형 클라이언트(ASP, DCOM, NT 서비스 등)에서의 Office 애플리케이션 자동화를 권장하지 않으며 지원도 하지 않는다는 점, 무인 환경에서 Office가 불안정한 동작이나 데드락을 일으킬 수 있다는 점, Open XML 등 대체 수단이 권장된다는 점에 대해.  2 3 4 5

  8. Microsoft Learn, Import-Csv. Import-Csv가 CSV에서 표 형식 커스텀 객체를 만든다는 점, 1행째가 헤더로 해석된다는 점, -Header로 헤더 없는 파일에 열 이름을 줄 수 있다는 점, 헤더 빈칸에는 H로 시작하는 임시 열 이름이 붙는다는 점, 값이 문자열로 읽힌다는 점에 대해.  2 3 4

  9. Microsoft Learn, Export-Csv. Export-Csv가 객체의 각 속성을 CSV 열로 쓴다는 점, PowerShell 7의 기본 인코딩이 UTF8NoBOM이라는 점, PowerShell 6.0 이후는 #TYPE 행이 기본으로 출력되지 않아 NoTypeInformation이 암묵이 된다는 점에 대해.  2 3

  10. Microsoft Learn, Install a package manager for PowerShell. PowerShell Gallery 접속에는 전송 계층 보안(TLS) 1.2 이상이 필요하다는 점, 세션에서 TLS 1.2를 켜는 명령과 이를 프로필 스크립트에 적어 두는 방법에 대해. 

  11. Microsoft Learn, Import data from Excel or export data to Excel with SQL Server Integration Services (SSIS). 무인·비대화형 환경에서의 Excel 구성 요소 이용이 지원되지 않는다는 점, 운영 자동 처리에서는 플랫 파일(CSV)이나 Open XML 기반 방식으로의 교체가 권장된다는 점에 대해. 

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

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

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

자주 묻는 질문

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

PowerShell로 CSV를 읽고 쓰면 글자가 깨지는 이유는 무엇인가요?
Windows PowerShell 5.1과 PowerShell 7의 기본 문자 인코딩이 다르기 때문입니다. 5.1은 명령어(cmdlet)마다 기본값이 제각각이라 Export-Csv는 ASCII(일본어가 사라짐), BOM 없는 파일의 Import-Csv는 UTF-8로 해석, Get-Content는 ANSI(일본어 환경에서는 Shift_JIS)입니다. 7은 일괄적으로 BOM 없는 UTF-8이 기본값입니다. 읽기·쓰기 모두에서 -Encoding을 반드시 명시하는 것이 유일한 안전책입니다.
두 개의 CSV를 대조(차이 확인)하려면 어떻게 해야 하나요?
'차이가 있는지 없는지'만 보려면 Compare-Object에 -Property로 키 열을 지정하는 것이 손쉽습니다. SideIndicator로 늘어난 행(=>)과 사라진 행(<=)을 알 수 있습니다. 다른 CSV에서 이름이나 부서 같은 열을 끌어와 결합하고 싶다면, 마스터 쪽을 키→행의 해시테이블로 만든 뒤 한 행씩 조회하는 것이 정석이며, 수만 건 규모에서도 빠르게 처리할 수 있습니다. 키를 찾지 못한 행은 그냥 버리지 말고 별도 파일로 출력해 사람이 확인할 수 있게 합니다.
PowerShell로 Excel 파일(xlsx)을 만드는 데 Excel 본체가 필요한가요?
필요하지 않습니다. 커뮤니티가 만든 ImportExcel 모듈을 사용하면 Excel이 설치되어 있지 않은 기기에서도 xlsx 읽기·쓰기, 테이블화, 서식 지정, 피벗 테이블 작성까지 가능합니다. PowerShell Gallery에서 Install-Module로 도입합니다. Excel 본체를 COM으로 조작하는 방법은 매크로 실행 등 'Excel 자체의 기능'이 필요한 경우의 최후 수단으로 자리매김하는 것이 안전합니다.
PowerShell에서 Excel을 COM으로 조작하면 왜 EXCEL.EXE가 남나요?
COM 객체에 대한 참조(RCW)가 해제되지 않은 채로 있으면 Quit을 호출해도 Excel 프로세스가 종료되지 않기 때문입니다. 워크북이나 셀 범위를 다룰 때마다 중간 객체에 대한 참조가 늘어나므로, 사용이 끝나면 Marshal.ReleaseComObject로 명시적으로 해제하고 GC.Collect로 회수를 촉진해야 합니다. 또한 Microsoft는 작업 스케줄러 등 무인 환경에서의 Office 자동화를 권장·지원하지 않으므로, 정기적으로 실행하고 싶은 처리는 ImportExcel처럼 Excel 본체가 필요 없는 방식으로 옮겨야 합니다.
CSV 집계는 Excel의 피벗 테이블과 PowerShell 중 어느 쪽으로 해야 하나요?
한 번만 하는 분석이라면 Excel로 충분합니다. 매주·매월 같은 절차를 반복한다면 Group-Object와 Measure-Object로 스크립트화할 가치가 있습니다. 절차가 문서가 아니라 코드로 남기 때문에 담당자가 바뀌어도 같은 결과를 재현할 수 있고, 작업 스케줄러를 이용한 정기 실행으로도 이어갈 수 있습니다. 집계 결과를 ImportExcel로 xlsx로 출력하면 받는 쪽은 평소 쓰던 Excel 파일로 취급할 수 있습니다.

저자 프로필

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

Go Komura

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

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

블로그 목록으로 돌아가기