이미지 크기 조정
이미지를 비동기적으로 다운로드하고 크기를 조정하는 방법을 보여줍니다.
이 예제는 QFuture, QPromise 및 QFutureWatcher 클래스를 사용하여 UI를 차단하지 않고 네트워크에서 이미지 모음을 다운로드하고 크기를 조정하는 방법을 보여줍니다.

이 애플리케이션은 다음 단계로 구성됩니다.
- 사용자가 지정한 URL 목록에서 이미지를 다운로드합니다.
- 이미지의 크기를 조정합니다.
- 크기 조정이 완료된 이미지를 그리드 레이아웃으로 표시합니다.
먼저 다운로드부터 시작해 보겠습니다:
QFuture<QByteArray> Images::download(const QList<QUrl> &urls)
{download() 메서드는 URL 목록을 받아 QFuture 를 반환합니다. QFuture 는 다운로드된 각 이미지에 대해 수신된 바이트 배열 데이터를 저장합니다. 이 데이터를 QFuture 내에 저장하기 위해, QPromise 객체를 생성하고 다운로드 시작을 알리기 위해 다운로드가 시작되었음을 보고합니다:
QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
promise->start();
...
return promise->future();
}프로미스와 연관된 퓨처가 호출자에게 반환됩니다.
아직 자세한 내용은 다루지 않겠지만, Promise 객체가 QSharedPointer 로 감싸져 있다는 점에 유의합시다. 이에 대해서는 나중에 설명하겠습니다.
QNetworkAccessManager 를 사용하여 각 URL에 대한 네트워크 요청을 보내고 데이터를 다운로드합니다:
for (const auto &url : urls) {
QSharedPointer<QNetworkReply> reply(qnam.get(QNetworkRequest(url)));
replies.push_back(reply);그리고 여기서부터 흥미로운 부분이 시작됩니다:
QtFuture::connect(reply.get(), &QNetworkReply::finished).then([=] {
if (promise->isCanceled()) {
if (!promise->future().isFinished())
promise->finish();
return;
}
if (reply->error() != QNetworkReply::NoError) {
if (!promise->future().isFinished())
throw reply->error();
}
promise->addResult(reply->readAll());
// Report finished on the last download
if (promise->future().resultCount() == urls.size())
promise->finish();
}).onFailed([promise] (QNetworkReply::NetworkError error) {
promise->setException(std::make_exception_ptr(error));
promise->finish();
}).onFailed([promise] {
const auto ex = std::make_exception_ptr(
std::runtime_error("Unknown error occurred while downloading."));
promise->setException(ex);
promise->finish();
});
}
...QObject::connect() 메서드를 사용하여 QNetworkReply 의 신호에 연결하는 대신, QtFuture::connect()를 사용합니다. 이 메서드는 QObject::connect()와 유사하게 작동하지만, QNetworkReply::finished() 신호가 발송되는 즉시 사용할 수 있게 되는 QFuture 객체를 반환합니다. 이를 통해 예제에서와 같이 연속 처리(continuation)와 오류 처리기를 연결할 수 있습니다.
.then()를 통해 연결된 연속 처리에서, 사용자가 다운로드 취소를 요청했는지 확인합니다. 그럴 경우 요청 처리를 중단합니다. QPromise::finish() 메서드를 호출하여 사용자에게 처리가 완료되었음을 알립니다. 네트워크 요청이 오류로 종료된 경우, 예외를 발생시킵니다. 이 예외는 .onFailed() 메서드를 사용하여 연결된 오류 핸들러에서 처리됩니다. 두 개의 오류 핸들러가 있다는 점에 유의하십시오. 첫 번째 핸들러는 네트워크 오류를 처리하고, 두 번째 핸들러는 실행 중에 발생한 기타 모든 예외를 처리합니다. 두 핸들러 모두 예외를 Promise 객체 내에 저장하고( download() 메서드를 호출한 측에서 처리하도록), 계산이 완료되었음을 보고합니다. 또한, 단순화를 위해 오류가 발생하면 진행 중인 모든 다운로드를 중단한다는 점에 유의하십시오.
요청이 취소되지 않았고 오류도 발생하지 않은 경우, 네트워크 응답에서 데이터를 읽어와 프롬프트 객체의 결과 목록에 추가합니다:
...
promise->addResult(reply->readAll());
// Report finished on the last download
if (promise->future().resultCount() == urls.size())
promise->finish();
...프로미스 객체 내에 저장된 결과의 개수가 다운로드해야 할 url의 개수와 같다면, 더 이상 처리할 요청이 없으므로 프로미스가 완료되었다고 보고합니다.
앞서 언급했듯이, 우리는 프로미스를 ` QSharedPointer` 내부로 감쌌습니다. 프로미스 객체는 각 네트워크 응답에 연결된 핸들러들 간에 공유되므로, 프로미스 객체를 복사하여 여러 곳에서 동시에 사용해야 합니다. 따라서 ` QSharedPointer `가 사용됩니다.
download() 메서드는 Images::process 메서드 내에서 호출됩니다. 이 메서드는 사용자가 “URL 추가” 버튼을 누를 때 호출됩니다:
...
connect(addUrlsButton, &QPushButton::clicked, this, &Images::process);
...이전 다운로드에서 남았을 수 있는 잔여 데이터를 지운 후, 사용자가 다운로드할 이미지의 URL을 지정할 수 있도록 대화 상자를 생성합니다. 지정된 URL 개수를 기반으로 이미지가 표시될 레이아웃을 초기화하고 다운로드를 시작합니다. download() 메서드가 반환한 퓨처는 저장되어, 사용자가 필요할 경우 다운로드를 취소할 수 있습니다:
void Images::process()
{
// Clean previous state
replies.clear();
addUrlsButton->setEnabled(false);
if (downloadDialog->exec() == QDialog::Accepted) {
const auto urls = downloadDialog->getUrls();
if (urls.empty())
return;
cancelButton->setEnabled(true);
initLayout(urls.size());
downloadFuture = download(urls);
statusBar->showMessage(tr("Downloading..."));
...다음으로, 크기 조정 단계를 처리하기 위해 연속 처리(continuation)를 연결합니다. 이에 대해서는 나중에 자세히 설명하겠습니다:
downloadFuture
.then([this](auto) {
cancelButton->setEnabled(false);
updateStatus(tr("Scaling..."));
scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
downloadFuture.results()));
})
...그 후 onCanceled() 및 onFailed() 핸들러를 연결합니다:
.onCanceled([this] {
updateStatus(tr("Download has been canceled."));
})
.onFailed([this](QNetworkReply::NetworkError error) {
updateStatus(tr("Download finished with error: %1").arg(error));
// Abort all pending requests
abortDownload();
})
.onFailed([this](const std::exception &ex) {
updateStatus(tr(ex.what()));
})
...사용자가 “취소” 버튼을 누르면 .onCanceled() 메서드를 통해 연결된 핸들러가 호출됩니다:
...
connect(cancelButton, &QPushButton::clicked, this, &Images::cancel);
...cancel() 메서드는 단순히 대기 중인 모든 요청을 중단합니다:
void Images::cancel()
{
statusBar->showMessage(tr("Canceling..."));
downloadFuture.cancel();
abortDownload();
}.onFailed() 메서드를 통해 연결된 핸들러는 이전 단계 중 하나에서 오류가 발생한 경우 호출됩니다. 예를 들어, 다운로드 단계에서 프로미스 내에 네트워크 오류가 저장된 경우, 이 오류는 QNetworkReply::NetworkError 를 인수로 받는 핸들러로 전달됩니다.
downloadFuture 가 취소되지 않았고 오류도 보고되지 않은 경우, 스케일링 후속 처리가 실행됩니다.
스케일링은 계산 부하가 클 수 있고 메인 스레드를 차단하고 싶지 않으므로, QtConcurrent::run()을 사용하여 새로운 스레드에서 스케일링 단계를 실행합니다.
scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
downloadFuture.results()));스케일링이 별도의 스레드에서 실행되므로, 사용자는 스케일링 작업이 진행 중인 동안 애플리케이션을 종료할 수도 있습니다. 이러한 상황을 원활하게 처리하기 위해, QtConcurrent::run() 이 반환한 QFuture 을 QFutureWatcher 인스턴스에 전달합니다.
워처의 QFutureWatcher::finished 시그널은 Images::scaleFinished 슬롯에 연결됩니다:
connect(&scalingWatcher, &QFutureWatcher<QList<QImage>>::finished,
this, &Images::scaleFinished);이 슬롯은 UI에 크기 조정된 이미지를 표시하고, 크기 조정 과정에서 발생할 수 있는 오류를 처리하는 역할을 합니다:
void Images::scaleFinished()
{
const OptionalImages result = scalingWatcher.result();
if (result.has_value()) {
const auto scaled = result.value();
showImages(scaled);
updateStatus(tr("Finished"));
} else {
updateStatus(tr("Failed to extract image data."));
}
addUrlsButton->setEnabled(true);
}오류 보고는 Images::scaled() 메서드에서 optional을 반환하는 방식으로 구현됩니다:
Images::OptionalImages Images::scaled(const QList<QByteArray> &data)
{
QList<QImage> scaled;
for (const auto &imgData : data) {
QImage image;
image.loadFromData(imgData);
if (image.isNull())
return std::nullopt;
scaled.push_back(image.scaled(100, 100, Qt::KeepAspectRatio));
}
return scaled;
}여기서 ` Images::OptionalImages ` 타입은 단순히 ` std::optional`에 대한 `typedef`입니다:
using OptionalImages = std::optional<QList<QImage>>;참고: .onFailed() 핸들러를 사용하여 비동기 크기 조정 작업에서 발생하는 오류를 처리할수 없습니다 . 핸들러는 UI 스레드 내의 Images 객체 컨텍스트에서 실행되어야 하기 때문입니다. 비동기 계산이 진행되는 동안 사용자가 애플리케이션을 종료하면 Images 객체가 소멸되며, 이 경우 연속 처리(continuation)에서 해당 객체의 멤버에 접근하면 크래시가 발생합니다. QFutureWatcher 와 그 신호를 사용하면 이 문제를 피할 수 있습니다. QFutureWatcher 가 소멸되면 신호가 분리되므로, 관련 슬롯은 소멸된 컨텍스트에서 절대 실행되지 않기 때문입니다.
나머지 코드는 간단합니다. 자세한 내용은 예제 프로젝트를 확인해 보세요.
예제 실행
다음 경로에서 예제를 실행할 수 있습니다:
- Qt Creator
Welcome 모드를 열고 Examples 에서 예제를 선택하십시오. 자세한 내용은 Qt Creator: 튜토리얼: 빌드 및 실행을 참조하십시오.
- Qt Extension for Visual Studio Code
Command Palette 에서 Qt: Open Qt examples 명령을 실행하고 목록에서 예제를 선택합니다. 자세한 내용은 Qt Extension for Visual Studio Code: 튜토리얼: 빌드 및 실행을 참조하십시오.
© 2026 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.