Dart 튜토리얼 13편: 파일·네트워크 I/O(dart:io·HTTP fetch)

요약#

핵심 요지#

  • 문제 정의: 파일을 읽거나 네트워크 요청을 보내는 코드는 결과가 “바로” 오지 않아서, 동기식으로 만들면 앱이 멈춘 것처럼 보이거나(블로킹) 에러 처리가 누락되기 쉽다.
  • 핵심 주장: dart:io1의 I/O는 Future2/Stream3 기반 비동기로 다루고, HTTP 요청은 플랫폼 종속인 HttpClient4보다 package:http5 같은 상위 라이브러리로 처리하는 흐름이 기준이 된다.
  • 주요 근거: 파일은 readAsString()6/readAsLines()7/openRead()8, 출력은 openWrite()9, 네트워크는 Uri10 + http.read()11/http.get()12 같은 예시로 구성된다.
  • 실무 기준: 작은 파일은 “한 번에 읽기”, 큰 파일은 “스트리밍으로 줄 단위 처리”, HTTP는 “URL 만들기 → 요청 → statusCode13 확인 → body14 사용”을 기본 틀로 둔다.

문서가 설명하는 범위#

  • dart:io에서 파일을 텍스트/바이너리로 읽고, 스트림으로 처리하고, 파일에 쓰는 방법
  • dart:io의 비동기 중심 설계(Future/Stream)와 에러 처리(try/catch) 흐름
  • package:http로 URL(Uri)을 만들고, HTTP 요청을 보내고, 응답(Response15)을 다루는 기본 패턴

읽는 시간: 18분 | 난이도: 초급


참고 자료#


문제 상황#

I/O 작업은 결과가 “나중에” 도착합니다.
파일을 읽거나 네트워크 요청을 보내는 순간, 앱은 기다리는 동안에도 다른 일을 해야 합니다.
이 흐름을 동기식으로 만들면 코드가 멈춘 것처럼 보이거나, 에러가 실행 중에 터지고, 처리 순서가 꼬이기 쉽습니다.
그래서 I/O는 처음부터 비동기(Future/Stream) 패턴으로 익히는 것이 안전합니다.


해결 방법#

단계 1: 작은 파일은 “한 번에 읽기”로 시작하기#

Why#

NOTE

설정 파일처럼 작은 텍스트 파일은 전체를 한 번에 읽어도 부담이 적고, 로직을 단순하게 시작할 수 있습니다.

import 'dart:io';
void main() async {
var config = File('config.txt');
// 파일을 문자열 하나로 읽는다.
var stringContents = await config.readAsString();
print('The file is ${stringContents.length} characters long.');
// 파일을 “줄 단위 문자열 리스트”로 읽는다.
var lines = await config.readAsLines();
print('The file is ${lines.length} lines long.');
}

What#

NOTE

File16readAsString()readAsLines()Future를 반환합니다.
즉, 파일을 읽는 동안에도 프로그램은 다른 작업을 진행할 수 있고, 결과는 “완료된 뒤”에 받게 됩니다.

How#

TIP

바이너리 파일(또는 텍스트라도 바이트로 다루고 싶을 때)은 readAsBytes()17로 한 번에 읽습니다.

import 'dart:io';
void main() async {
var config = File('config.txt');
var contents = await config.readAsBytes();
print('The file is ${contents.length} bytes long.');
}

Watch out#

WARNING

파일이 없거나 권한이 없으면 예외가 발생할 수 있습니다.
에러가 “잡히지 않은 예외”로 튀지 않게 try/catch로 묶어둡니다.

import 'dart:io';
void main() async {
var config = File('config.txt');
try {
var contents = await config.readAsString();
print(contents);
} catch (e) {
print(e);
}
}

결론: 작은 파일은 “한 번에 읽기”로 시작하고, 항상 에러 처리를 함께 둡니다.


단계 2: 큰 파일은 Stream으로 “조금씩 읽기”로 전환하기#

Why#

NOTE

파일이 크면 전체를 한 번에 메모리에 올리기 어렵습니다.
이때는 openRead()로 바이트 스트림을 만들고, 처리 가능한 단위(예: 줄)로 잘라가며 읽는 편이 안전합니다.

What#

NOTE

openRead()Stream<List<int>> 형태의 바이트 스트림을 반환합니다.
바이트를 텍스트로 바꾸려면 utf8.decoder18LineSplitter19 같은 변환기를 스트림에 적용합니다.

How#

TIP

바이트 스트림을 줄 단위 문자열 스트림으로 바꾼 뒤, await for20로 하나씩 처리합니다.

import 'dart:io';
import 'dart:convert';
void main() async {
var config = File('config.txt');
Stream<List<int>> inputStream = config.openRead();
var lines = utf8.decoder.bind(inputStream).transform(const LineSplitter());
try {
await for (final line in lines) {
print('Got ${line.length} characters from stream');
}
print('file is now closed');
} catch (e) {
print(e);
}
}

Watch out#

WARNING

dart:io는 웹 앱에서는 사용할 수 없습니다.
웹이 아닌 Flutter 앱, 명령줄 스크립트, 서버에서만 사용할 수 있습니다.

결론: 큰 파일은 Stream으로 처리 단위를 정해 “조금씩” 처리합니다.


단계 3: 파일 출력은 IOSink로 “쓰기 → flush → close”까지 마무리하기#

Why#

NOTE

로그처럼 “조금씩 계속 쓰는 출력”은 파일을 열고 반복해서 쓰는 흐름이 필요합니다.
이때 openWrite()IOSink21를 얻고, 마지막에 정리까지 해야 데이터가 안전하게 남습니다.

What#

NOTE

openWrite()는 파일에 쓰기 위한 IOSink를 제공합니다.
기본 모드는 FileMode.write22로 기존 내용을 덮어쓰며, 이어쓰기라면 FileMode.append23를 사용합니다.

How#

TIP

파일에 텍스트를 쓴 뒤 flush()24close()25로 마무리합니다.

import 'dart:io';
Future<void> writeLog() async {
var logFile = File('log.txt');
var sink = logFile.openWrite();
sink.write('FILE ACCESSED ${DateTime.now()}\n');
await sink.flush();
await sink.close();
}

이어쓰기 모드도 같은 방식으로 시작합니다.

var sink = logFile.openWrite(mode: FileMode.append);

Watch out#

WARNING

close()를 호출하지 않으면, 버퍼에 남은 내용이 파일에 반영되지 않을 수 있습니다.
즉, “쓰기만 하고 끝”이 아니라, “마무리까지”를 코드로 고정해야 합니다.

결론: 출력은 IOSink로 쓰고, 마지막에 flush()/close()까지 끝냅니다.


단계 4: HTTP 요청은 Uri + package:http로 “요청 → 상태 코드 확인 → 본문 사용” 흐름을 고정하기#

Why#

NOTE

네트워크 요청은 실패가 흔합니다.
성공/실패를 구분하지 않고 본문만 쓰면, 에러 상황에서 잘못된 값이 조용히 흘러갈 수 있습니다.
그래서 응답 전체(Response)를 받고, statusCode로 성공 여부를 먼저 확인하는 흐름이 필요합니다.

What#

NOTE

package:http는 HTTP 요청을 위한 라이브러리입니다.
Uri로 URL을 만들고, http.get() 같은 함수로 요청을 보낸 뒤, Response에서 statusCodebody를 사용합니다.

How#

TIP

먼저 의존성을 추가하고(dart pub add http), 가져온 데이터를 출력해 봅니다.

Terminal window
$ dart pub add http
import 'package:http/http.dart' as http;
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final httpPackageResponse = await http.get(httpPackageUrl);
if (httpPackageResponse.statusCode != 200) {
print('Failed to retrieve the http package!');
return;
}
print(httpPackageResponse.body);
}

필요하다면 headersMap<String, String> 형태로 함께 보낼 수 있습니다.

await http.get(
Uri.https('dart.dev', '/f/packages/http.json'),
headers: {'User-Agent': '<product name>/<product-version>'},
);

Watch out#

WARNING

HTTP 요청은 시간이 걸릴 수 있으므로 비동기(Future)로 동작합니다.
그리고 요청을 “여러 번” 보낼 때는 Client26를 유지하고, 마지막에 close()27로 정리하는 흐름이 제시됩니다.

import 'package:http/http.dart' as http;
void main() async {
final httpPackageUrl = Uri.https('dart.dev', '/f/packages/http.json');
final client = http.Client();
try {
final httpPackageInfo = await client.read(httpPackageUrl);
print(httpPackageInfo);
} finally {
client.close();
}
}

결론: HTTP는 Uri로 URL을 만들고, 응답의 statusCode를 먼저 확인하는 흐름으로 시작합니다.

Footnotes#

  1. dart(dart): 파일/디렉터리/프로세스/소켓/HTTP 등 I/O API를 제공하는 라이브러리다.

  2. Future(퓨처): 나중에 완료되는 작업의 결과를 표현하는 타입이다.

  3. Stream(스트림): 시간이 지나면서 여러 이벤트/값이 들어오는 흐름을 표현하는 타입이다.

  4. HttpClient(HTTP 클라이언트): dart:io에 있는 HTTP 클라이언트 클래스다.

  5. package(package): HTTP 요청을 위한 상위 수준 라이브러리다.

  6. readAsString()(문자열로 읽기): 파일 전체를 문자열로 읽어 Future<String>을 돌려주는 메서드다.

  7. readAsLines()(줄 단위로 읽기): 파일을 줄 단위 문자열 리스트로 읽어 Future<List<String>>을 돌려주는 메서드다.

  8. openRead()(스트림으로 읽기): 파일을 바이트 스트림(Stream<List<int>>)으로 여는 메서드다.

  9. openWrite()(쓰기 스트림 열기): 파일에 쓰기 위한 IOSink21를 여는 메서드다.

  10. Uri(유알아이): URL을 표현하고 조합하는 클래스다.

  11. http.read()(본문만 읽기): 요청이 성공하면 본문 문자열을 돌려주고, 실패하면 예외를 던지는 함수다.

  12. http.get()(응답 받기): 응답 전체(Response15)를 돌려주는 함수다.

  13. statusCode(상태 코드): HTTP 응답의 성공/실패 등을 나타내는 숫자 코드다.

  14. body(본문): HTTP 응답의 본문 문자열이다.

  15. Response(리스폰스): HTTP 응답 객체로, 상태 코드와 본문 등을 포함한다. 2

  16. File(파일): 파일 경로를 기준으로 읽기/쓰기 작업을 제공하는 클래스다.

  17. readAsBytes()(바이트로 읽기): 파일 전체를 바이트 리스트로 읽어 Future<List<int>>를 돌려주는 메서드다.

  18. utf8.decoder(UTF-8 디코더): 바이트 스트림을 문자열 스트림으로 바꾸는 변환기다.

  19. LineSplitter(라인 스플리터): 문자열 스트림을 줄 단위로 나누는 변환기다.

  20. await for(await for): Stream3 이벤트를 비동기 반복문으로 하나씩 받는 문법이다.

  21. IOSink(I/O 싱크): 파일/소켓 등에 데이터를 쓰는 sink 타입이다. 2

  22. FileMode.write(쓰기 모드): 기존 파일 내용을 덮어쓰는 파일 쓰기 모드다.

  23. FileMode.append(추가 모드): 파일 끝에 이어서 쓰는 파일 쓰기 모드다.

  24. flush()(플러시): 버퍼에 남은 데이터를 즉시 내보내는 메서드다.

  25. close()(닫기): sink를 닫고 자원을 정리하는 메서드다.

  26. Client(클라이언트): 여러 요청을 보낼 때 재사용할 수 있는 HTTP 클라이언트다.

  27. close()(클라이언트 닫기): Client를 닫아 자원을 정리하는 메서드다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Dart 튜토리얼 13편: 파일·네트워크 I/O(dart:io·HTTP fetch)
https://moodturnpost.net/posts/dart/dart-io-network-fetch/
작성자
Moodturn
게시일
2026-01-04
Moodturn

목차