“매달 기간계 시스템에서 내려받은 CSV를 Excel로 열어, 피벗으로 집계하고, 체재를 정리해서 메일로 보낸다”, “두 시스템에서 나온 명부를 대조해 차이를 눈으로 찾는다” ── 이런 종류의 정형 업무 상담을 자주 받습니다. 한 번에 걸리는 시간은 30분이라도 매주·매월·여러 사람이 반복하면 연간으로는 상당한 시간이 사라지고, 수작업 복사·붙여넣기에는 반드시 실수가 섞여 들어갑니다.
PowerShell은 이 영역과 궁합이 좋은 도구입니다. Windows에 기본으로 들어 있고(Windows PowerShell 5.1), CSV를 ‘객체의 표’로 읽어들여 집계・대조・출력까지 파이프라인으로 연결할 수 있습니다. 게다가 커뮤니티가 만든 ImportExcel 모듈을 사용하면 Excel 본체 없이도 xlsx 장표까지 만들 수 있습니다.
한편 이 분야에는 일본어(한국어) 환경 특유의 지뢰가 있습니다. 문자 코드의 기본값이 Windows PowerShell 5.1과 PowerShell 7에서 완전히 다르기 때문에 “내 PC에서는 동작했는데 다른 환경에서는 글자가 깨졌다”는 일이 빈번하게 일어납니다. 이 글에서는 중소기업의 정보 시스템・업무 담당자가 CSV・Excel 업무를 PowerShell로 자동화할 때의 실무 레시피를, 문자 코드의 함정부터 순서대로 정리합니다.
전제 지식: 대상은 중소기업의 정보 시스템・업무 담당자이지만, 이 글은 PowerShell 입문 글이 아닙니다. 변수와 파이프라인, foreach나 if를 쓰는 방법, 스크립트 파일(.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
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.csv에 E003이 나와 있다는 것을 알아챌 수 있는지 여부가 이 처리를 돌리는 의미 그 자체입니다.
4.3. 열 이름의 흔들림과 헤더 처리
현장의 CSV는 “사원 번호”, “사번”, “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는 “그냥 텍스트”가 아니다 ── C# 업무 앱의 CSV 실무(문자 코드・Excel 호환・인젝션 대책)
- PowerShell 실용 명령어 모음 ── 일상 업무에서 자주 쓰는 작은 기능 늘리기
- Excel 장표 출력을 어떻게 만들까 - COM 자동화 / Open XML / 템플릿 방식의 판단표
- C#의 Excel 조작에서 EXCEL.EXE가 남는 문제 ── COM 참조 해제 패턴과 교체 판단
- Windows의 문자 코드와 개행 코드를 정리한다 - Shift_JIS / UTF-8 / UTF-16, 문자 깨짐, CRLF / LF, 왜 혼란스러운가
- Windows PowerShell 5.1과 PowerShell 7의 차이 ── 사내 스크립트 이행 실무 가이드
관련 상담 영역
합동회사 코무라소프트는 CSV・Excel을 매개로 한 정형 업무 자동화 스크립트 작성, Excel COM 의존 장표 처리에서의 탈피(ImportExcel/Open XML로의 대체), “특정 환경에서만 글자가 깨진다”, “EXCEL.EXE가 남는다” 같은 결함 조사를 다루고 있습니다.
참고 링크
-
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
-
Microsoft Learn, Group-Object. Group-Object가 지정한 속성의 값으로 객체를 그룹화하고, 그룹별 건수와 요소를 반환한다는 점, -AsHashTable로 키→그룹의 해시테이블을 얻을 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Measure-Object. Measure-Object로 건수 외에 Sum・Average・Maximum・Minimum(및 표준편차)을 계산할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Compare-Object. Compare-Object가 두 객체 집합을 비교해, SideIndicator(<= / => / ==)로 어느 쪽에만 존재하는지 나타낸다는 점, -Property로 지정한 열만 비교할 수 있다는 점, 기준 쪽・비교 대상 쪽이 null인 경우 종료 오류가 된다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
PowerShell Gallery, ImportExcel. 커뮤니티 제작 ImportExcel 모듈의 배포처. Excel 본체를 설치하지 않고도 xlsx 읽기・쓰기・피벗 테이블・서식 지정 등이 가능한 모듈이라는 점, Install-Module을 이용한 도입 방법, 배포 페이지에 최소 요구 PowerShell 버전 기재가 없다는 점에 대해. ↩ ↩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
-
Microsoft Learn, Marshal.ReleaseComObject(Object) Method. ReleaseComObject가 COM 객체에 연결된 RCW(런타임 호출 가능 래퍼)의 참조 카운트를 줄여 COM 객체의 수명을 명시적으로 제어하는 데 쓰인다는 점, 해제된 객체에 접근하면 예외가 발생하므로 사용에 주의가 필요하다는 점에 대해. ↩ ↩2
-
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
-
Microsoft Learn, Import-Csv. Import-Csv가 CSV에서 표 형식의 커스텀 객체를 만든다는 점, 1행째가 헤더로 해석된다는 점, -Header로 헤더 없는 파일에 열 이름을 줄 수 있다는 점, 헤더의 빈칸에는 H로 시작하는 임시 열 이름이 붙는다는 점, 값이 문자열로 읽힌다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Export-Csv. Export-Csv가 객체의 각 속성을 CSV의 열로 써낸다는 점, PowerShell 7에서의 기본 인코딩이 UTF8NoBOM이라는 점, PowerShell 6.0 이후는 #TYPE 행이 기본적으로 출력되지 않아 NoTypeInformation이 암묵적이 된다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Install a package manager for PowerShell. PowerShell Gallery 접속에는 전송 계층 보안(TLS) 1.2 이상이 필요하다는 점, 세션에서 TLS 1.2를 활성화하는 명령과 이를 프로필 스크립트에 적어 두는 방법에 대해. ↩
-
Microsoft Learn, Import data from Excel or export data to Excel with SQL Server Integration Services (SSIS). 무인・비대화형 환경에서의 Excel 구성 요소 이용이 지원되지 않는다는 점, 운영 자동 처리에서는 플랫 파일(CSV)이나 Open XML 기반 방식으로의 대체가 권장된다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
PowerShell에서 COM과 .NET을 호출하는 실전 ── 스크립트가 닿는 범위를 단숨에 넓힌다
PowerShell에서 .NET 클래스를 호출하는 방법, Add-Type을 통한 C#과 Win32 API 통합, COM 조작, Excel 프로세스 잔류와 뒷정리, Office 무인 실행이 비지원인 이유, 5.1과 7의 차이까지 실무 관점에서 해설...
PowerShell 스크립트의 인수 설계와 모듈화 ── 「작동하는 스크립트」에서 「남에게 넘길 수 있는 스크립트」로
PowerShell 스크립트를 다른 사람에게 넘길 수 있는 품질로 끌어올리는 절차를 정리합니다. param 블록과 [CmdletBinding()], 입력 검증, 파이프라인 입력, -WhatIf 대응, .psm1 모듈화, 사내 공유와 Git 관리의...
Windows PowerShell 5.1과 PowerShell 7의 차이 ── 사내 스크립트 이행 실무 가이드
Windows PowerShell 5.1과 PowerShell 7의 관계(공존과 pwsh.exe), 5.1은 신기능을 추가하지 않는다는 공식 방침, 인코딩 차이로 인한 문자 깨짐, #Requires를 통한 방어, 작업 스케줄러 갱신까지 이행 절차...
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로 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 파일로 취급할 수 있습니다.