FileButton (파일버튼)
Va.FileButton — 파일 선택 다이얼로그를 여는 버튼
클릭하면 OS의 파일 선택 다이얼로그가 열리는 버튼입니다. <input type="file">을 숨겨두고 버튼을 클릭 트리거로 삼는 흔한 패턴을, 컴포넌트 하나로 캡슐화한 것.
- 클래스: Va.FileButton — va_component.js:12256
- short name: fileButton
- 상속: Va.Component
- isContainer: false
- 기본 아이콘: ico_attach (첨부 아이콘)
- 베이스 CSS: va-button (일반 Button과 스타일 계보 공유)

1. 기본 사용
{
tagName: 'fileButton',
text: '파일 첨부',
onChange: 'onFileSelected' // 파일 선택 완료 시
}
onFileSelected(component, element, files, evt) {
// files: FileList (0..N개)
for (let i = 0; i < files.length; i++) {
console.log(files[i].name, files[i].size, files[i].type);
}
}
기본으로 ico_attach 아이콘이 좌측에 붙어 있어, text 없이도 첨부 버튼임을 시각적으로 알립니다.
2. 다른 버튼과의 차이
항목Va.ButtonVa.FileButton
| 클릭 동작 | 사용자 정의 콜백 | 파일 다이얼로그 자동 오픈 + change 이벤트 |
| 기본 아이콘 | 없음 | ico_attach |
| 전용 이벤트 | click / iconClick / contextmenu / focus / blur | click / change / contextmenu / focus / blur |
| 숨겨진 요소 | 없음 | <input type="file"> (display: none) |
| iconClick 이벤트 | ✓ | ✕ |
| pressedBackgroundColor / pressedColor | ✓ | ✕ |
한 줄 요약: "클릭하면 파일 고르는 창이 뜨는 버튼."
3. 주요 속성
표시 (Button 공통 규약)
속성기본값설명
| text / innerHTML | — | 라벨 |
| icon | 'ico_attach' | 좌측 아이콘 (기본값 있음) |
| iconPosition | 'left' | 아이콘 위치 (left/right/top/bottom) |
| iconOnly | false | 텍스트 숨김 |
| iconSize | — | 아이콘 크기 |
| iconRotate | — | 아이콘 회전(°) |
| textAlign | — | 텍스트 정렬 |
| appearance | — | 시각 스타일 (default/primary/secondary/tertiary/subtle/outline/transparent/accent/inverted 등) |
| shape | — | rounded/circular/square |
| size | — | xsmall~xlarge |
| disabled | — | 비활성화 |
| readonly | — | 읽기 전용 |
| badge | — | 배지 부착 |
| backgroundColor | — | 배경색 직접 지정 |
⚠️ 주의: properties에 선언은 있지만 실질 구현이 없거나 미묘한 항목:
- active / selected — 선언은 있으나 명시적 시각 스타일 세팅 미미
- pressedBackgroundColor / pressedColor — Va.Button과 달리 update()에 처리 로직 없음
파일 관련 속성 — 누락됨
일반 <input type="file">이 갖는 속성들이 컴포넌트 API로 노출되어 있지 않습니다:
- accept (.jpg,.png,image/* 같은 필터)
- multiple (다중 선택)
- capture (모바일 카메라 직접)
필요하면 생성 후 component.fileElement.setAttribute('accept', 'image/*') 같은 식으로 JS로 직접 붙여야 합니다. 데이터를 옵션으로 넘겨서 자동 설정하는 통로가 없다는 게 이 컴포넌트의 아쉬운 점.
4. 이벤트
이벤트시그니처발생 시점
| change | (component, element, files, evt) | 파일 선택 완료 — files는 FileList |
| click | (component, element, evt) | 버튼 클릭 (파일 다이얼로그 열기 직전) |
| focus / blur | (component, element, evt) | 포커스 진입/이탈 |
| contextmenu | (component, element, evt) | 우클릭 |
change 이벤트가 핵심
onFileSelected(component, element, files, evt) {
// FormData에 담아 서버로 전송
const formData = new FormData();
for (let i = 0; i < files.length; i++) {
formData.append('file', files[i]);
}
this.formService(this, '/api/upload', formData, this.onUploaded);
}
⚠️ 취소 시 이벤트 안 옴 — 사용자가 다이얼로그를 그냥 닫으면 change가 발생하지 않습니다 (표준 파일 input 동작).
⚠️ 같은 파일 재선택이 안 될 수 있음 — 표준 <input type="file"> 동작 상 같은 파일을 두 번 선택하면 두 번째는 change 안 옴. 필요하면 처리 후 component.fileElement.value = ''로 초기화.
⚠️ click 이벤트가 두 번 바인딩됨 — va_component.js:12292-12296와 va_component.js:12313-12315 두 곳에서 click을 바인딩 → click 이벤트가 한 번의 클릭에 두 번 dispatch될 수 있습니다. 콜백에서 중복 실행 주의.
5. 메서드
메서드설명
| setText(text) / setInnerHTML(html) | 라벨 변경 |
| setIcon(icon) | 아이콘 변경 |
| setAppearance(v) / setShape(v) / setSize(v) | 시각 속성 변경 |
| setDisabled(bool) | 비활성화 |
| setReadonly(bool) | 읽기 전용 (⚠️ 구현이 this.disabled = readonly로 되어 있어 실질적으로 disabled와 동일) |
| setActive(active) | (인자 무시하고 항상 true 세팅 — 버그성 코드) |
| focus() / blur() | 포커스 제어 |
공식 API로는 없지만 유용한 것들 (직접 접근):
- component.fileElement.value = '' — 선택된 파일 초기화 (같은 파일 재선택 가능하게)
- component.fileElement.setAttribute('accept', 'image/*') — 파일 타입 필터
- component.fileElement.setAttribute('multiple', 'multiple') — 다중 선택
- component.fileElement.click() — 프로그래매틱하게 다이얼로그 열기
6. 내부 구조
방금 수정한 버그 반영 후 정상 구조:
<button elname="element" tabindex="0" class="va-button [appearance] [shape] [size]">
<span elname="inner" class="button-inner">
<input type="file" elname="file" style="display:none"> ← 숨겨진 실제 입력
<span elname="icon" class="icon ico_attach [left|right|top|bottom]">…</span>
<span elname="text" class="text">라벨</span>
<span cpname="badge">…</span> ← badge 지정 시
</span>
</button>
핵심 트릭: 사용자에게는 <button>만 보이지만, 클릭 시 내부에서 숨겨진 <input type="file">에 프로그래매틱하게 click()을 발사해 다이얼로그를 엽니다 (va_component.js:12292-12296):
this._bindEvent(this.element,'click', (evt) => {
this.dispatchEvent('click', this, evt);
const event = new MouseEvent("click");
this.fileElement.dispatchEvent(event); // ← 숨은 input 클릭
});
7. 언제 쓰나
FileButton이 맞을 때
- 단순 파일 첨부 버튼 (게시글, 채팅, 프로필 이미지)
- "파일 선택" 액션이 명확한 버튼으로 필요할 때
- 아이콘·라벨·appearance 자유롭게 커스터마이즈해야 할 때
다른 걸 쓸 때
- 드래그 앤 드롭 지원 필요 → Va.FileDropUpload (va_component.js:12504)
- 파일 목록·진행률 표시가 함께 필요 → FileDropUpload 조합
- 파일 표시가 아니라 텍스트/폼 값 입력 → Va.Input, Va.Textarea
- 이미지 크롭·프리뷰까지 → 별도 커스텀 필요
8. 알아두면 좋을 주의사항 (실사용 팁)
- click 콜백이 중복 발화 위험 — 위 이벤트 섹션 참조. 콜백 로직이 idempotent하지 않으면 _alreadyClicked 플래그 같은 가드 필요.
- change는 취소 시 안 옴 — "선택했다"만 감지 가능, "취소했다"는 알 수 없음.
- 같은 파일 재선택 안 됨 → 필요 시 fileElement.value = ''로 초기화.
- accept / multiple 지정 불편 — 옵션으로 노출 안 되어 있어 생성 후 DOM 조작 필요.
- setReadonly()는 사실상 setDisabled() — 별개 상태로 관리 안 됨.
- badge의 위치 계산이 setTimeout(500) 기반 — 버튼 폭이 초기 렌더링 후 바뀌면 배지 위치가 잠깐 어긋날 수 있음.
- 방금 수정한 버그 — 이전 버전에서는 element.textContent = text로 이너 구조를 파괴하고 있어서 여백·아이콘이 사라졌고 파일 다이얼로그도 detached input에 click을 쏘는 상황이었음. 지금은 정상.
9. Va.Button으로도 만들 수 있는 거 아닌가?
가능은 합니다. 직접 <input type="file">을 만들어 숨기고, Button의 onClick에서 그 input에 click()을 쏘면 됩니다. 하지만:
- 파일 input 관리(초기화, DOM 부착)를 매번 손으로
- 배지·아이콘·appearance 조합을 다시 조립
- change 이벤트 리스너 연결
- 여러 곳에서 반복 → 캡슐화 필요
… 이 과정을 미리 해둔 게 FileButton입니다. 얇은 래퍼지만 반복 작업을 줄여줍니다.