본문 바로가기
빌드노트 검색

빌드노트

Export-Csv에 Name 대신 ClassId가 저장되는 이유

Format-Table 뒤에 Export-Csv를 붙이면 Name·Count 대신 포맷 객체의 내부 속성이 CSV 열이 된다. PowerShell 7.6.6에서 두 합성 객체의 헤더·Import-Csv 값·타입을 비교하고, 원본 저장과 화면 표시를 분리한다.

PowerShell에서 표로는 잘 보이던 데이터를 CSV로 저장했는데, 첫 열이 Name이 아니라 ClassId2e4f51ef21dd47e99d3c952918aff9cd처럼 나올 수 있다. 이때는 Export-Csv 앞의 Format-Table부터 확인한다. Format-Table은 다음 명령에 원본 데이터가 아니라 표를 표현하는 포맷 객체를 보낸다. 원래 객체를 CSV로 저장하고, 화면에 표를 그리는 명령은 따로 실행하면 된다. Export-Csv 공식 문서의 설명과 예제 7 (새 탭에서 열림)도 이 차이를 다룬다.

같은 두 행을 파일과 화면으로 보낸다

다음은 Windows의 PowerShell 7.6.6에서 직접 비교한 예다. PowerShell 7을 pwsh -NoProfile로 열고, 쓰기 가능한 새 빈 폴더를 현재 작업 위치로 삼아 실행한다. 추가 패키지는 필요 없다. 뒤의 비교까지 실행하면 이 폴더에 direct.csv와 formatted.csv 두 파일을 만든다. -NoClobber는 같은 이름의 기존 파일을 덮어쓰지 않게 한다. 기존 파일 보호 옵션 (새 탭에서 열림)

먼저 Name과 Count를 가진 두 객체를 직접 내보낸 뒤, 같은 데이터를 화면에 표시한다.

$rows = @(
    [pscustomobject]@{ Name = 'alpha'; Count = 2 }
    [pscustomobject]@{ Name = 'beta'; Count = 5 }
)
$rows | Export-Csv -LiteralPath ./direct.csv -NoTypeInformation -Encoding utf8 -NoClobber
$rows | Format-Table Name, Count

화면에는 다음 표가 나왔다. CSV 저장과 표시는 서로 다른 파이프라인이다.

Name  Count
----  -----
alpha     2
beta      5

이번에는 같은 $rows에서 Format-Table의 출력을 CSV로 보내고, 두 파일의 헤더를 읽는다.

$rows | Format-Table Name, Count |
    Export-Csv -LiteralPath ./formatted.csv -NoTypeInformation -Encoding utf8 -NoClobber

Get-Content -LiteralPath ./direct.csv -Encoding utf8 -TotalCount 1
Get-Content -LiteralPath ./formatted.csv -Encoding utf8 -TotalCount 1

실제 헤더는 다음과 같았다. 첫 줄이 직접 내보낸 파일이고, 둘째 줄이 포맷 명령을 거친 파일이다.

"Name","Count"
"ClassId2e4f51ef21dd47e99d3c952918aff9cd","pageHeaderEntry","pageFooterEntry","autosizeInfo","shapeInfo","groupingEntry"

두 명령 모두 UTF-8과 -NoTypeInformation을 명시했다. 그런데 포맷 명령을 거친 쪽만 내부 속성이 열 이름이 됐다. 이 비교에서는 인코딩이나 타입 정보 헤더 옵션을 바꿀 문제가 아니라, Export-Csv가 받는 객체를 바꿔야 하는 문제다.

다시 읽어도 Name과 Count가 있는지 확인한다

파일이 만들어졌다는 사실만으로는 원하는 데이터를 저장했다고 판단할 수 없다. 다음 코드는 두 CSV를 읽어 정상 파일의 값을 확인하고, 문제 파일에 Name과 Count 속성이 실제로 있는지 검사한다.

$direct = @(Import-Csv -LiteralPath ./direct.csv -Encoding utf8)
$viaFormat = @(Import-Csv -LiteralPath ./formatted.csv -Encoding utf8)

$direct | ForEach-Object { '{0}={1}' -f $_.Name, $_.Count }
$null -eq $viaFormat[0].PSObject.Properties['Name']
$null -eq $viaFormat[0].PSObject.Properties['Count']

출력은 다음과 같았다.

alpha=2
beta=5
True
True

직접 저장한 파일에서는 두 행의 이름과 수를 읽을 수 있었다. 마지막 두 True는 포맷 명령을 거친 파일의 첫 객체에 해당 이름의 속성이 없다는 뜻이다. 원래 값이 빈 문자열인지를 검사한 것이 아니다. 정상 파일도 이번 Import-Csv 결과의 Count 타입은 System.String이었다. 여기서 확인한 것은 CSV의 열과 값이며, 원래 정수 타입까지 복원됐다는 뜻은 아니다.

문제 파일의 헤더만 Name,Count로 바꿔서는 이 두 행의 데이터 구조를 되찾을 수 없다. CSV를 만드는 코드를 수정한 뒤 원본 객체에서 다시 내보낸다. 기존 파일을 확인 중이라면 먼저 헤더를 읽어 본다. 헤더가 이미 Name,Count라면 이 예제와 같은 포맷 객체 문제로 단정하지 말고, 내보내기 전 데이터와 CSV를 읽는 쪽을 이어 확인한다.

표처럼 보이는 출력과 데이터 객체는 다르다

Format-Table Name, Count가 열을 골라 주는 것처럼 보여도, 그 출력을 데이터 가공 단계로 사용해서는 안 된다. Format-Table의 Outputs 설명 (새 탭에서 열림)은 표를 표현하는 포맷 객체를 반환한다고 명시한다.

다음 코드는 원본의 첫 객체와 포맷 결과의 첫 객체 타입을 비교한다. 새 CSV를 만들지는 않는다.

$rows[0].GetType().FullName
$formatObjects = @($rows | Format-Table Name, Count)
$formatObjects[0].GetType().FullName

같은 환경에서 다음 두 타입을 확인했다.

System.Management.Automation.PSCustomObject
Microsoft.PowerShell.Commands.Internal.Format.FormatStartData

$rows에는 여전히 원래 객체가 있었다. 바뀐 것은 Format-Table이 다음 명령으로 내보내는 객체다. 위의 두 번째 줄은 그 포맷 결과를 $formatObjects에 저장한 것이고, 앞의 잘못된 내보내기는 같은 결과를 Export-Csv에 전달한 것이다. 내부 타입 이름 전체를 코드에 고정할 필요는 없다. 여기서는 파이프라인의 값이 원본 데이터와 다른 종류라는 점을 확인한다.

내보낼 열을 고를 목적이라면 Export-Csv 앞에 Select-Object Name, Count를 사용한다. 공식 Export-Csv 설명 (새 탭에서 열림)에서도 속성 선택에는 Select-Object를 안내한다. 화면용 Format-Table은 첫 예제처럼 별도 명령으로 실행한다. 이미 $rows = ... | Format-Table처럼 변수에 포맷 결과를 담았다면, 포맷 명령 이전의 조회부터 다시 실행해 원본 데이터 변수를 확보해야 한다.

이 비교는 PowerShell 7.6.6에서 새로 만든 두 PSCustomObject를 사용한 별도 재현이다. Windows PowerShell 5.1과 다른 운영체제에서는 실행하지 않았다. 내부 열 이름과 포맷 레코드의 구성을 모든 버전의 고정 형식으로 가정하지 않고, 자신의 파일에서 기대하는 헤더와 읽어 들인 값을 확인한다.

← 전체 글 보기