성능 관련 고려 사항 및 제안
타이밍 고려 사항
애플리케이션 개발자는 일반적으로 렌더링 엔진이 초당 60 프레임의 일관된 재생률을 달성할 수 있도록 노력합니다. 사용하는 하드웨어와 요구 사항에 따라 이 수치는 달라질 수 있지만, 60 FPS는 매우 일반적인 수치입니다. 60 FPS는 각 프레임 사이에 약 16밀리초의 처리 시간이 있다는 것을 의미하며, 여기에는 드로우 프리미티브를 그래픽 하드웨어로 업로드하는 데 필요한 처리도 포함됩니다.
실무적으로 이는 애플리케이션 개발자가 다음을 수행해야 함을 의미합니다.
- 가능한 한 비동기식, 이벤트 주도형 프로그래밍을 사용해야 하며
- 중요한 처리는 워커 스레드를 사용하여 수행해야 합니다
- 절대 이벤트 루프를 수동으로 회전시키지 말아야 합니다
- 블로킹 함수 내에서 프레임당 2밀리초 이상 소요하지 말 것
이를 준수하지 않을 경우 프레임이 건너뛰게 되어 사용자 경험에 심각한 영향을 미치게 됩니다.
참고: 유혹적이지만 절대 사용해서는 안 되는패턴은 , QML에서 호출되는 C++와 같은 백엔드 코드 블록 내에서 차단 현상을 피하기 위해 자체적인 ` QEventLoop `를 생성하거나 ` QCoreApplication::processEvents()`를 호출하는 것입니다. 이는 위험한 방법입니다. 시그널 핸들러나 바인딩에서 이벤트 루프에 진입하면 QML 엔진은 다른 바인딩, 애니메이션, 전환 등을 계속 실행하기 때문입니다. 이러한 바인딩은 예를 들어 이벤트 루프가 포함된 계층 구조를 파괴하는 등의 부작용을 일으킬 수 있습니다.
프로파일링
가장 중요한 팁은 다음과 같습니다. QML ProfilerQt Creator 에 포함된 (또는 Qt Extension for Visual Studio Code 의 추적 뷰어)를 사용하십시오. 애플리케이션에서 시간이 어디에 소요되는지 파악하면, 잠재적으로 문제가 될 수 있는 영역보다는 실제로 존재하는 문제 영역에 집중할 수 있습니다. 자세한 내용은 Qt Creator: QML 애플리케이션 프로파일링 및 Qt Extension for Visual Studio Code: QML 코드 프로파일링을 참조하십시오.
어떤 바인딩이 가장 자주 실행되는지, 또는 애플리케이션에서 어떤 함수에 가장 많은 시간이 소요되는지 파악하면, 문제 영역을 최적화해야 할지, 아니면 성능을 향상시키기 위해 애플리케이션의 일부 구현 세부 사항을 재설계해야 할지 결정할 수 있습니다. 프로파일링 없이 코드를 최적화하려고 시도하면, 상당한 성능 향상보다는 아주 미미한 개선만 얻을 가능성이 높습니다.
JavaScript 코드
대부분의 QML 애플리케이션에는 속성 바인딩 표현식, 함수, 시그널 핸들러 등의 형태로 일부 JavaScript 코드가 포함되어 있습니다. 이는 일반적으로 문제가 되지 않습니다. Qt Quick Compiler덕분에 간단한 함수와 바인딩은 매우 빠르게 실행될 수 있습니다. 그러나 불필요한 처리가 실수로 트리거되지 않도록 주의해야 합니다. QML Profiler 는 자바스크립트 실행 및 실행을 유발한 원인에 대한 상세한 정보를 보여줄 수 있습니다.
형 변환
자바스크립트 사용 시 발생하는 주요 비용 중 하나는, QML 타입의 속성에 접근할 때 경우에 따라 기본이 되는 C++ 데이터(또는 이에 대한 참조)를 포함하는 외부 리소스를 가진 자바스크립트 객체가 생성된다는 점입니다. 대부분의 경우 이 작업은 비용이 적게 들지만, 경우에 따라 상당히 큰 비용이 발생할 수 있습니다. 크고 복잡한 값형이나 시퀀스형을 다룰 때는 주의가 필요합니다. 이러한 타입은 제자리에서 변경하거나 다른 속성에 할당할 때마다 QML 엔진에 의해 복사되어야 합니다. 이로 인해 병목 현상이 발생하면, 대신 객체형을 사용하는 것을 고려해 보십시오. 객체 유형의 리스트는 값 유형의 리스트와 같은 문제를 일으키지 않습니다. 객체 유형의 리스트는 ` QQmlListProperty`를 사용하여 구현되기 때문입니다.
단순 값 유형 간의 대부분의 변환은 비용이 적게 듭니다. 하지만 예외도 있습니다. 문자열로부터 URL을 생성하는 과정에는 QUrl 인스턴스를 생성하는 작업이 포함될 수 있으며, 이는 비용이 많이 듭니다.
속성 해결
속성 해결에는 시간이 소요됩니다. 조회 작업은 일반적으로 후속 실행 시 훨씬 빠르게 수행되도록 최적화되어 있지만, 가능하다면 불필요한 작업을 아예 피하는 것이 항상 가장 좋습니다.
다음 예제에는 자주 실행되는 코드 블록이 있습니다(이 경우 명시적 루프의 내용이지만, 예를 들어 자주 평가되는 바인딩 표현식일 수도 있습니다). 이 블록 내에서 “rect” ID를 가진 객체와 그 “color” 속성을 여러 번 해결합니다:
// bad.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which: string, value: real) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
printValue("red", rect.color.r);
printValue("green", rect.color.g);
printValue("blue", rect.color.b);
printValue("alpha", rect.color.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
}rect.color 가 조회될 때마다 QML 엔진은 다음 작업을 수행해야 합니다:
- JavaScript 힙에 값형 래퍼를 할당합니다.
- Rectangle 의
color속성에 대한 게터(getter)를 실행합니다. - 결과로 얻은 ` QColor `를 값 유형 래퍼로 복사합니다.
이 작업을 4번 반복할 필요는 없습니다. 대신 블록 내에서 공통 기반을 한 번만 해결하면 됩니다:
// good.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which: string, value: real) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
var rectColor = rect.color; // resolve the common base.
printValue("red", rectColor.r);
printValue("green", rectColor.g);
printValue("blue", rectColor.b);
printValue("alpha", rectColor.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
}이 간단한 변경만으로도 성능이 크게 향상됩니다. 또한, 위 코드는 (루프 처리 중 조회되는 속성이 절대 변경되지 않기 때문에) 다음과 같이 속성 해결을 루프 밖으로 호이스팅하여 더욱 개선할 수 있습니다:
// better.qml
import QtQuick
Item {
width: 400
height: 200
Rectangle {
id: rect
anchors.fill: parent
color: "blue"
}
function printValue(which: string, value: real) {
console.log(which + " = " + value);
}
Component.onCompleted: {
var t0 = new Date();
var rectColor = rect.color; // resolve the common base outside the tight loop.
for (var i = 0; i < 1000; ++i) {
printValue("red", rectColor.r);
printValue("green", rectColor.g);
printValue("blue", rectColor.b);
printValue("alpha", rectColor.a);
}
var t1 = new Date();
console.log("Took: " + (t1.valueOf() - t0.valueOf()) + " milliseconds for 1000 iterations");
}
}속성 바인딩
속성 바인딩 표현식은 참조하는 속성 중 하나라도 변경되면 재평가됩니다. 따라서 바인딩 표현식은 가능한 한 단순하게 유지해야 합니다.
어떤 처리를 수행하는 루프가 있는데, 그 처리의 최종 결과만 중요한 경우, 누적 과정의 중간 단계에서 바인딩 표현식의 재평가가 유발되는 것을 피하기 위해, 속성 자체를 점진적으로 업데이트하는 것보다 임시 누적 변수를 업데이트한 뒤, 이를 나중에 업데이트해야 할 속성에 할당하는 편이 더 나은 경우가 많습니다.
다음의 인위적인 예제는 이 점을 잘 보여줍니다:
// bad.qml
import QtQuick
Item {
id: root
width: 200
height: 200
property int accumulatedValue: 0
Text {
anchors.fill: parent
text: root.accumulatedValue.toString()
onTextChanged: console.log("text binding re-evaluated")
}
Component.onCompleted: {
var someData = [ 1, 2, 3, 4, 5, 20 ];
for (var i = 0; i < someData.length; ++i) {
accumulatedValue = accumulatedValue + someData[i];
}
}
}onCompleted 핸들러 내의 루프로 인해 "text" 속성 바인딩이 6번 재평가됩니다(이로 인해 text 값에 의존하는 다른 모든 속성 바인딩과 onTextChanged 신호 핸들러도 매번 재평가되며, 매번 표시할 텍스트가 레이아웃됩니다). 이 경우, 우리가 실제로는 누적의 최종 값에만 관심이 있으므로 이는 분명히 불필요한 작업입니다.
다음과 같이 다시 작성할 수 있습니다:
// good.qml
import QtQuick
Item {
id: root
width: 200
height: 200
property int accumulatedValue: 0
Text {
anchors.fill: parent
text: root.accumulatedValue.toString()
onTextChanged: console.log("text binding re-evaluated")
}
Component.onCompleted: {
var someData = [ 1, 2, 3, 4, 5, 20 ];
var temp = accumulatedValue;
for (var i = 0; i < someData.length; ++i) {
temp = temp + someData[i];
}
accumulatedValue = temp;
}
}시퀀스 관련 팁
앞서 언급했듯이, 값 유형의 시퀀스는 신중하게 다뤄야 합니다.
첫째, 시퀀스 타입은 다음 두 가지 서로 다른 시나리오에서 다른 동작을 보입니다:
- 시퀀스가 ` QObject `의 ` Q_PROPERTY `인 경우(이를 참조 시퀀스라고 부르겠습니다),
- 시퀀스가 QObject 의 Q_INVOKABLE 함수에서 반환되는 경우(이를 ‘복사 시퀀스’라고 부르겠습니다).
참조 시퀀스는 JavaScript 코드 내 또는 원본 객체에서 변경될 때마다 QMetaObject 를 통해 읽기 및 쓰기 작업이 수행됩니다. 최적화를 위해 참조 시퀀스(및 참조 값 유형)는 지연 로딩될 수 있습니다. 실제 내용은 해당 시퀀스가 처음 사용될 때만 가져옵니다. 즉, JavaScript에서 시퀀스 내 요소의 값을 변경하면 다음과 같은 결과가 발생합니다:
복사 시퀀스는 실제 시퀀스가 자바스크립트 객체의 리소스 데이터에 저장되므로 훨씬 더 간단하며, 읽기/수정/쓰기 사이클이 발생하지 않습니다(대신 리소스 데이터가 직접 수정됩니다).
따라서 참조 시퀀스의 요소에 대한 쓰기 작업은 복사 시퀀스의 요소에 대한 쓰기 작업보다 훨씬 느립니다. 사실, N개 요소로 구성된 참조 시퀀스의 단일 요소에 쓰기 작업은 해당 참조 시퀀스에 N개 요소로 구성된 복사 시퀀스를 할당하는 것과 비용 면에서 동일하므로, 계산 중에는 임시 복사 시퀀스를 수정한 다음 그 결과를 참조 시퀀스에 할당하는 것이 일반적으로 더 좋습니다.
다음과 같은 C++ 타입이 존재하며(그리고 “Qt.example” 네임스페이스에 미리 등록되어 있다고 가정합니다):
class SequenceTypeExample : public QQuickItem
{
Q_OBJECT
Q_PROPERTY (QList<qreal> qrealListProperty READ qrealListProperty WRITE setQrealListProperty NOTIFY qrealListPropertyChanged)
public:
SequenceTypeExample() : QQuickItem() { m_list << 1.1 << 2.2 << 3.3; }
~SequenceTypeExample() {}
QList<qreal> qrealListProperty() const { return m_list; }
void setQrealListProperty(const QList<qreal> &list) { m_list = list; emit qrealListPropertyChanged(); }
signals:
void qrealListPropertyChanged();
private:
QList<qreal> m_list;
};다음 예제는 타이트한 루프 내에서 참조 시퀀스의 요소들에 쓰기 작업을 수행하므로 성능이 저하됩니다:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
qrealListProperty.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
qrealListProperty[j] = j;
}
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}"qrealListProperty[j] = j" 표현식으로 인해 내부 루프에서 발생하는 QObject 속성의 읽기 및 쓰기 작업은 이 코드를 매우 비효율적으로 만듭니다. 대신, 기능적으로는 동일하지만 훨씬 더 빠른 방법은 다음과 같습니다:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
var someData = [1.1, 2.2, 3.3]
someData.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
someData[j] = j;
}
qrealListProperty = someData;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}피해야 할 또 다른 일반적인 패턴은 각 요소를 읽은 후 수정하고 시퀀스 속성에 다시 쓰는 ‘읽기-수정-쓰기(read-modify-write)’ 루프입니다. 앞의 예제와 마찬가지로, 이 방식은 매 반복마다 QObject 속성에 대한 읽기 및 쓰기 작업을 유발합니다:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
qrealListProperty.length = 100;
for (var i = 0; i < 500; ++i) {
for (var j = 0; j < 100; ++j) {
qrealListProperty[j] = qrealListProperty[j] * 2;
}
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}대신, 시퀀스의 수동 복사본을 생성하고, 복사본을 수정한 다음, 그 결과를 속성에 다시 할당하십시오:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
width: 200
height: 200
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 500; ++i) {
let data = [...qrealListProperty];
for (var j = 0; j < 100; ++j) {
data[j] = data[j] * 2;
}
qrealListProperty = data;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}둘째, 속성의 요소 중 하나라도 변경되면 해당 속성에 대한 변경 신호가 발생합니다. 시퀀스 속성의 특정 요소에 대한 바인딩이 많은 경우, 해당 요소에 바인딩된 동적 속성을 생성하고, 바인딩 표현식에서 시퀀스 요소 대신 그 동적 속성을 심볼로 사용하는 것이 좋습니다. 이렇게 하면 해당 속성의 값이 변경될 때만 바인딩이 재평가되기 때문입니다.
이는 대부분의 클라이언트가 결코 마주치지 않을 드문 사용 사례이지만, 다음과 같은 작업을 수행하게 될 경우를 대비하여 알아두면 좋습니다:
// bad.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
property int firstBinding: qrealListProperty[1] + 10;
property int secondBinding: qrealListProperty[1] + 20;
property int thirdBinding: qrealListProperty[1] + 30;
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
qrealListProperty[2] = i;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}루프 내에서 인덱스 2에 있는 요소만 수정되더라도, 변경 신호의 세분화 수준이 속성 전체가 변경된 것으로 간주되기 때문에 세 개의 바인딩이 모두 재평가된다는 점에 유의하십시오. 따라서 중간 바인딩을 추가하는 것이 때로는 유용할 수 있습니다:
// good.qml
import QtQuick
import Qt.example
SequenceTypeExample {
id: root
property int intermediateBinding: qrealListProperty[1]
property int firstBinding: intermediateBinding + 10;
property int secondBinding: intermediateBinding + 20;
property int thirdBinding: intermediateBinding + 30;
Component.onCompleted: {
var t0 = new Date();
for (var i = 0; i < 1000; ++i) {
qrealListProperty[2] = i;
}
var t1 = new Date();
console.log("elapsed: " + (t1.valueOf() - t0.valueOf()) + " milliseconds");
}
}위의 예제에서는 매번 중간 바인딩만 재평가되므로 성능이 크게 향상됩니다.
값형(Value-Type) 팁
값형 속성(font, color, vector3d 등)은 시퀀스형 속성과 유사한 QObject 속성 및 변경 알림 동작을 가집니다. 따라서 위에서 시퀀스에 대해 제시한 팁은 값형 속성에도 적용됩니다. 일반적으로 값형에서는 이러한 문제가 덜 발생하지만(값형의 하위 속성 수는 시퀀스의 요소 수보다 훨씬 적기 때문), 불필요하게 재평가되는 바인딩의 수가 증가하면 성능에 부정적인 영향을 미치게 됩니다.
일반적인 성능 관련 팁
언어 설계로 인해 발생하는 일반적인 자바스크립트 성능 고려 사항은 QML에도 적용됩니다. 그중 가장 두드러진 사항은 다음과 같습니다:
- 가능한 한 eval() 사용을 피하십시오
- 객체의 속성을 삭제하지 마십시오
일반적인 인터페이스 요소
텍스트 요소
텍스트 레이아웃을 계산하는 작업은 시간이 오래 걸릴 수 있습니다. 가능한 한 StyledText 대신 PlainText 형식을 사용하는 것을 고려하십시오. 이렇게 하면 레이아웃 엔진이 처리해야 하는 작업량을 줄일 수 있습니다. PlainText 을 사용할 수 없는 경우(이미지를 삽입해야 하거나, 전체 텍스트가 아닌 특정 문자 범위에만 굵게, 기울임꼴 등의 서식을 지정하기 위해 태그를 사용해야 하는 경우 등)에는 StyledText 을 사용해야 합니다.
AutoText 는 텍스트가 (아마 그렇지 않겠지만) StyledText 일 가능성이 있는 경우에만 사용해야 합니다. 이 모드는 구문 분석 비용이 발생하기 때문입니다. RichText 모드는 사용하지 않는 것이 좋습니다. StyledText 가 훨씬 적은 비용으로 이 모드의 기능을 거의 모두 제공하기 때문입니다.
이미지
이미지는 모든 사용자 인터페이스에서 필수적인 요소입니다. 하지만 안타깝게도, 로딩에 걸리는 시간, 메모리 소비량, 그리고 사용 방식 등으로 인해 문제가 발생하는 주요 원인이 되기도 합니다.
비동기 로딩
이미지는 대개 용량이 상당히 크기 때문에, 이미지 로딩으로 인해 UI 스레드가 차단되지 않도록 하는 것이 현명합니다. QML Image 요소의 "asynchronous" 속성을 true 로 설정하면, 사용자 인터페이스의 미관에 부정적인 영향을 미치지 않는 한도 내에서 로컬 파일 시스템의 이미지를 비동기적으로 로드할 수 있습니다(원격 이미지는 항상 비동기적으로 로드됩니다).
"asynchronous" 속성이 true 로 설정된 Image 요소는 낮은 우선순위의 워커 스레드에서 이미지를 로드합니다.
명시적 소스 크기
애플리케이션에서 큰 이미지를 로드하되 작은 크기의 요소에 표시하는 경우, "sourceSize" 속성을 렌더링되는 요소의 크기로 설정하면 큰 이미지 대신 축소된 버전의 이미지가 메모리에 유지되도록 할 수 있습니다.
sourceSize를 변경하면 이미지가 다시 로드되므로 주의하십시오.
런타임 합성 피하기
또한 애플리케이션에 미리 합성된 이미지 리소스를 포함시킴으로써(예: 그림자 효과가 적용된 요소 제공) 런타임 합성 작업을 피할 수 있다는 점도 기억해 두십시오.
이미지 스무딩 피하기
필요한 경우에만 ‘ image.smooth ’ 기능을 활성화하십시오. 일부 하드웨어에서는 처리 속도가 느려질 수 있으며, 이미지가 원래 크기로 표시될 경우 시각적인 효과가 나타나지 않습니다.
그리기
동일한 영역을 여러 번 그리지는 않도록 하십시오. 배경을 여러 번 그리는 것을 방지하려면 Rectangle 대신 Item을 루트 요소로 사용하십시오.
앵커를 사용하여 요소 배치하기
항상 서로에 대해 상대적인 위치를 지정할 때는 바인딩보다 앵커를 사용하는 것이 더 효율적입니다. 다음 예시에서 바인딩을 사용하여 rect2를 rect1에 대해 위치시키는 방법을 살펴보세요:
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
x: rect1.x
y: rect1.y + rect1.height
width: rect1.width - 20
height: 200
}앵커를 사용하면 이를 더 효율적으로 구현할 수 있습니다:
Rectangle {
id: rect1
x: 20
width: 200; height: 200
}
Rectangle {
id: rect2
height: 200
anchors.left: rect1.left
anchors.top: rect1.bottom
anchors.right: rect1.right
anchors.rightMargin: 20
}바인딩을 이용한 위치 지정(앵커를 사용하는 대신 시각적 객체의 x, y, width 및 height 속성에 바인딩 표현식을 할당하는 방식)은 최대의 유연성을 제공하지만, 상대적으로 느립니다.
레이아웃이 동적이지 않은 경우, 레이아웃을 지정하는 가장 성능이 좋은 방법은 x, y, width 및 height 속성을 정적으로 초기화하는 것입니다. 항목의 좌표는 항상 부모를 기준으로 하므로, 부모의 0,0 좌표로부터 고정된 오프셋을 두고 싶다면 앵커를 사용해서는 안 됩니다. 다음 예제에서 자식 Rectangle 객체들은 같은 위치에 있지만, 표시된 앵커 코드는 정적 초기화를 통해 고정 위치를 지정하는 코드만큼 리소스 효율적이지 않습니다:
Rectangle {
width: 60
height: 60
Rectangle {
id: fixedPositioning
x: 20
y: 20
width: 20
height: 20
}
Rectangle {
id: anchorPositioning
anchors.fill: parent
anchors.margins: 20
}
}모델과 뷰
대부분의 애플리케이션에는 뷰에 데이터를 제공하는 모델이 적어도 하나씩 있습니다. 애플리케이션 개발자가 최대 성능을 달성하기 위해서는 몇 가지 의미론적 사항을 숙지해야 합니다.
사용자 정의 C++ 모델
QML의 뷰와 함께 사용하기 위해 C++과 같은 백엔드 언어로 자체 커스텀 모델을 작성하는 것이 바람직한 경우가 많습니다. 이러한 모델의 최적 구현 방식은 해당 모델이 수행해야 할 사용 사례에 따라 크게 달라지겠지만, 몇 가지 일반적인 지침은 다음과 같습니다:
- 가능한 한 비동기적으로 구현하십시오
- 모든 처리는 (낮은 우선순위의) 워커 스레드에서 수행하십시오
- (속도가 느릴 수 있는) I/O 및 IPC를 최소화할 수 있도록 백엔드 작업을 일괄 처리하십시오
GUI 스레드의 리소스 부족 현상(이로 인해 체감 성능이 저하될 수 있음)의 위험을 최소화하기 위해 낮은 우선순위의 워커 스레드를 사용하는 것이 권장된다는 점을 유의해야 합니다. 또한, 동기화 및 잠금 메커니즘이 성능 저하의 주요 원인이 될 수 있으므로, 불필요한 잠금을 피하기 위해 주의를 기울여야 합니다.
ListModel QML 타입
Qt Qml Models는 ListView 에 데이터를 공급하는 데 사용할 수 있는 ListModel 유형을 제공합니다. 이는 빠른 프로토타이핑에는 유용하지만, 대량의 데이터에는 적합하지 않습니다. 필요한 경우 적절한 QAbstractItemModel 를 사용하십시오.
워커 스레드 내에서 데이터 채우기
ListModel JavaScript에서 요소들은 (낮은 우선순위의) 워커 스레드 내에서 채울 수 있습니다. 변경 사항을 메인 스레드와 동기화하려면 개발자가 WorkerScript 내부에서 ListModel 에 대해 sync() 를 명시적으로 호출해야 합니다. 자세한 내용은 WorkerScript 문서를 참조하십시오.
WorkerScript 요소를 사용하면(자바스크립트 엔진은 스레드별로 작동하므로) 별도의 자바스크립트 엔진이 생성된다는 점에 유의하십시오. 이로 인해 메모리 사용량이 증가하게 됩니다. 그러나 여러 개의 ` WorkerScript ` 요소는 모두 동일한 워커 스레드를 사용하므로, 애플리케이션이 이미 하나의 ` WorkerScript ` 요소를 사용하고 있는 경우 두 번째나 세 번째 요소를 사용하는 데 따른 메모리 영향은 미미합니다. 반면, 추가된 워커 스크립트는 병렬로 실행되지 않습니다.
동적 역할은 사용하지 마십시오
ListModel 요소는 최적화 목적으로 주어진 모델 내 각 요소의 역할 유형이 안정적이라고 가정합니다. 요소마다 유형이 동적으로 변경될 수 있다면 모델의 성능이 크게 저하될 것입니다.
따라서 동적 타입 지정은 기본적으로 비활성화되어 있으며, 개발자는 모델의 ` dynamicRoles ` 부울 속성을 명시적으로 설정해야만 동적 타입 지정을 활성화할 수 있습니다(이 경우 성능 저하가 수반됩니다). 절대적으로 필요한 경우가 아니면 동적 타입 지정을 사용하지 않는 것이 좋습니다.
뷰
뷰 델리게이트는 가능한 한 단순하게 유지해야 합니다. 델리게이트 내에는 필요한 정보를 표시하는 데 필요한 최소한의 QML만 포함해야 합니다. 당장 필요하지 않은 추가 기능(예: 클릭 시 더 많은 정보를 표시하는 기능)은 필요할 때까지 구현하지 않아야 합니다(다음 섹션의 지연 초기화 참조).
다음 목록은 델리게이트를 설계할 때 유의해야 할 사항들을 잘 요약한 것입니다:
- 델리게이트에 포함된 요소가 적을수록 생성 속도가 빨라지므로, 뷰를 더 빠르게 스크롤할 수 있습니다.
- 델리게이트 내 바인딩의 수는 최소한으로 유지하십시오. 특히, 델리게이트 내 상대적 위치 지정을 위해 바인딩보다는 앵커를 사용하십시오.
- 델리게이트 내부에서는 ShaderEffect 요소를 사용하지 마십시오.
- 델리게이트에서 클리핑을 절대 활성화하지 마십시오.
뷰의 ` cacheBuffer ` 속성을 설정하여 가시 영역 밖에서 델리게이트의 비동기적 생성 및 버퍼링을 허용할 수 있습니다. 비단순하고 단일 프레임 내에서 생성될 가능성이 낮은 뷰 델리게이트의 경우, ` cacheBuffer `을 사용하는 것이 좋습니다.
cacheBuffer 는 추가적인 델리게이트를 메모리에 유지한다는 점을 유의하십시오. 따라서 cacheBuffer 를 활용하여 얻는 이점과 추가적인 메모리 사용량 사이에는 균형을 맞춰야 합니다. cacheBuffer 를 사용할 경우 발생하는 메모리 부하 증가로 인해 드물게 스크롤 시 프레임 속도가 저하될 수 있으므로, 개발자는 벤치마킹을 통해 해당 사용 사례에 가장 적합한 값을 찾아야 합니다.
성능을 더욱 향상시키려면 뷰에서 항목 재사용을 활성화하는 것을 고려해 보십시오. 자세한 내용은 Reusing Items for ListView 및 Reusing Items for TableView and TreeView 을 참조하십시오.
시각 효과
Qt Quick 에는 개발자와 디자이너가 매우 매력적인 사용자 인터페이스를 만들 수 있도록 해주는 여러 기능이 포함되어 있습니다. 유동성과 역동적인 전환 효과, 그리고 시각 효과는 애플리케이션에서 큰 효과를 낼 수 있지만, QML의 일부 기능을 사용할 때는 성능에 영향을 미칠 수 있으므로 주의가 필요합니다.
애니메이션
일반적으로 속성에 애니메이션을 적용하면 해당 속성을 참조하는 모든 바인딩이 재평가됩니다. 대개는 이것이 바람직한 결과이지만, 경우에 따라 애니메이션을 실행하기 전에 바인딩을 비활성화한 다음, 애니메이션이 완료된 후에 바인딩을 다시 할당하는 것이 더 나을 수도 있습니다.
애니메이션이 진행되는 동안 자바스크립트 실행을 피하십시오. 예를 들어, x 속성 애니메이션의 각 프레임마다 복잡한 자바스크립트 표현식을 실행하는 것은 피해야 합니다.
개발자는 스크립트 애니메이션을 사용할 때 각별히 주의해야 합니다. 이러한 애니메이션은 메인 스레드에서 실행되기 때문에(따라서 완료하는 데 너무 오래 걸릴 경우 프레임이 건너뛸 수 있습니다).
입자
이 Qt Quick Particles 모듈을 사용하면 아름다운 파티클 효과를 사용자 인터페이스에 매끄럽게 통합할 수 있습니다. 하지만 플랫폼마다 그래픽 하드웨어 성능이 다르기 때문에, Particles 모듈은 하드웨어가 원활하게 지원할 수 있는 범위로 매개변수를 제한할 수 없습니다. 렌더링하려는 파티클의 수가 많을수록(그리고 크기가 클수록), 60 FPS로 렌더링하기 위해서는 더 빠른 그래픽 하드웨어가 필요합니다. 더 많은 입자에 영향을 주려면 더 빠른 CPU가 필요합니다. 따라서 대상 플랫폼에서 모든 입자 효과를 신중하게 테스트하여 60 FPS로 렌더링할 수 있는 입자의 수와 크기를 조정하는 것이 중요합니다.
불필요한 시뮬레이션을 방지하기 위해, 사용하지 않을 때(예: 보이지 않는 요소에서) 파티클 시스템을 비활성화할 수 있다는 점에 유의해야 합니다.
더 자세한 정보는 ‘파티클 시스템 성능 가이드’를 참조하십시오.
엘리먼트 수명 제어
애플리케이션을 단순하고 모듈화된 구성 요소로 분할하고, 각 구성 요소를 단일 QML 파일에 포함시킴으로써 애플리케이션 시작 시간을 단축하고 메모리 사용량을 더 효과적으로 제어할 수 있으며, 애플리케이션 내에서 활성화되어 있지만 보이지 않는 요소의 수를 줄일 수 있습니다.
지연 초기화
QML 엔진은 컴포넌트의 로딩 및 초기화로 인해 프레임이 건너뛰어지는 일이 없도록 하기 위해 몇 가지 정교한 처리를 수행합니다. 하지만 불필요한 작업을 피하고, 필요한 시점까지 작업을 미루는 것보다 시작 시간을 단축하는 더 좋은 방법은 없습니다. 이는 ` Loader` 중 하나를 사용하여 달성할 수 있습니다.
Loader 사용
Loader는 컴포넌트를 동적으로 로드하고 언로드할 수 있게 해주는 요소입니다.
- Loader의 "active" 속성을 사용하면, 필요할 때까지 초기화를 미룰 수 있습니다.
- "setSource()" 함수의 오버로드된 버전을 사용하면 초기 속성 값을 지정할 수 있습니다.
- 로더 asynchronous 의 속성을 true로 설정하면 컴포넌트가 인스턴스화되는 동안의 반응성을 향상시킬 수도 있습니다.
사용되지 않는 요소 제거
비 가시 요소의 자식 요소라서 보이지 않는 요소들 (예를 들어, 첫 번째 탭이 표시되어 있는 상태에서 탭 위젯의 두 번째 탭)은 대부분의 경우 지연 초기화되어야 하며, 더 이상 사용되지 않을 때 삭제되어야 합니다. 그래야만 해당 요소를 활성 상태로 두는 데 따르는 지속적인 비용(예: 렌더링, 애니메이션, 속성 바인딩 평가 등)을 피할 수 있습니다.
Loader 요소로 로드된 항목은 Loader의 "source" 또는 "sourceComponent" 속성을 재설정하여 해제할 수 있으며, 다른 항목들은 해당 항목에 대해 destroy()를 호출하여 명시적으로 해제할 수 있습니다. 경우에 따라 항목을 활성 상태로 유지해야 할 수도 있는데, 이 경우 최소한 보이지 않게 처리해야 합니다.
활성 상태이지만 보이지 않는 요소에 대한 자세한 내용은 다음 '렌더링' 섹션을 참조하십시오.
렌더링
에서 렌더링에 사용되는 씬 그래프는 Qt Quick 에서 렌더링에 사용되는 씬 그래프는 매우 역동적이고 애니메이션이 적용된 사용자 인터페이스를 60 FPS로 부드럽게 렌더링할 수 있게 해줍니다. 그러나 렌더링 성능을 급격히 저하시킬 수 있는 몇 가지 요인이 있으므로, 개발자는 가능한 한 이러한 함정을 피하도록 주의해야 합니다.
클리핑
클리핑은 기본적으로 비활성화되어 있으며, 필요한 경우에만 활성화해야 합니다.
클리핑은 시각적 효과일 뿐, 최적화가 아닙니다. 이는 렌더러의 복잡성을 감소시키는 것이 아니라 오히려 증가시킵니다. 클리핑이 활성화되면, 항목은 자체 페인팅은 물론 자식 노드의 페인팅까지 바운딩 사각형에 맞춰 잘라냅니다. 이로 인해 렌더러가 요소의 그리기 순서를 자유롭게 재정렬할 수 없게 되어, 최상의 경우에도 최적화되지 않은 씬 그래프 탐색이 발생합니다.
델리게이트 내부의 클리핑은 특히 문제가 심각하므로, 어떤 대가를 치르더라도 피해야 합니다.
오버드로잉 및 보이지 않는 요소
다른 (불투명한) 요소에 완전히 가려진 요소가 있다면, 해당 요소의 "visible" 속성을 false 로 설정하는 것이 가장 좋습니다. 그렇지 않으면 불필요하게 그려지게 됩니다.
마찬가지로, 보이지 않지만(예: 첫 번째 탭이 표시된 상태에서 탭 위젯의 두 번째 탭) 시작 시점에 초기화되어야 하는 요소(예: 두 번째 탭을 인스턴스화하는 데 걸리는 시간이 너무 길어 탭이 활성화된 시점에야 비로소 인스턴스화할 수 있는 경우), 해당 요소의 "visible" 속성을 false 로 설정하여 렌더링 비용을 피해야 합니다(앞서 설명한 바와 같이, 해당 요소는 여전히 활성 상태이므로 애니메이션이나 바인딩 평가에 따른 비용은 여전히 발생합니다).
반투명 대 불투명
불투명 콘텐츠는 일반적으로 반투명 콘텐츠보다 렌더링 속도가 훨씬 빠릅니다. 그 이유는 반투명 콘텐츠에는 블렌딩이 필요하고, 렌더러가 불투명 콘텐츠를 더 잘 최적화할 수 있기 때문입니다.
투명 픽셀이 하나만 포함된 이미지는 대부분 불투명하더라도 완전히 투명한 것으로 처리됩니다. 투명한 테두리가 있는 BorderImage 도 마찬가지입니다.
셰이더
ShaderEffect 유형을 사용하면 Qt Quick 애플리케이션에 GLSL 코드를 인라인으로 삽입할 수 있으며, 이로 인한 오버헤드는 매우 적습니다. 그러나 렌더링되는 도형의 모든 픽셀에 대해 프래그먼트 프로그램이 실행되어야 한다는 점을 유의해야 합니다. 저사양 하드웨어에 배포할 때 셰이더가 많은 픽셀을 처리하는 경우, 성능 저하를 방지하기 위해 프래그먼트 셰이더를 몇 개의 명령어로만 제한해야 합니다.
GLSL로 작성된 셰이더를 사용하면 복잡한 변환과 시각 효과를 구현할 수 있지만, 신중하게 사용해야 합니다. ShaderEffectSource 를 사용하면 장면이 그려지기 전에 FBO로 미리 렌더링됩니다. 이러한 추가 오버헤드는 상당히 큰 부담이 될 수 있습니다.
메모리 할당 및 회수
애플리케이션이 할당할 메모리의 양과 그 할당 방식은 매우 중요한 고려 사항입니다. 메모리 용량이 제한된 장치에서 발생할 수 있는 메모리 부족 현상에 대한 명백한 우려는 차치하더라도, 힙에 메모리를 할당하는 작업은 계산적으로 상당히 비용이 많이 드는 작업이며, 특정 할당 전략은 페이지 간 데이터 조각화를 증가시킬 수 있습니다. JavaScript는 자동으로 가비지 컬렉션이 수행되는 관리형 메모리 힙을 사용하며, 이는 몇 가지 장점이 있지만 동시에 중요한 함의도 내포하고 있습니다.
QML로 작성된 애플리케이션은 C++ 힙과 자동으로 관리되는 자바스크립트 힙 모두의 메모리를 사용합니다. 애플리케이션 개발자는 성능을 극대화하기 위해 각각의 미묘한 차이점을 잘 파악해야 합니다.
QML 애플리케이션 개발자를 위한 팁
이 섹션에 포함된 팁과 제안 사항은 단지 지침일 뿐이며, 모든 상황에 적용될 수는 없습니다. 최선의 결정을 내리기 위해 경험적 지표를 활용하여 애플리케이션을 신중하게 벤치마킹하고 분석해야 합니다.
컴포넌트를 지연 생성 및 초기화하십시오
애플리케이션이 여러 뷰(예: 여러 탭)로 구성되어 있지만 한 번에 하나만 필요한 경우, 지연 인스턴스화를 사용하여 특정 시점에 할당해야 하는 메모리 양을 최소화할 수 있습니다. 자세한 내용은 앞의 ‘지연 초기화’ 섹션을 참조하십시오.
사용되지 않는 객체 소멸
컴포넌트를 지연 로드하거나 JavaScript 표현식 실행 중에 객체를 동적으로 생성하는 경우, 자동 가비지 컬렉션이 처리할 때까지 기다리기보다는 수동으로 ` destroy() `을 호출하여 객체를 파괴하는 것이 더 나은 경우가 많습니다. 자세한 내용은 앞의 ‘요소 수명 제어’ 섹션을 참조하십시오.
가비지 컬렉터를 수동으로 호출하지 마십시오
대부분의 경우, 가비지 컬렉터를 수동으로 호출하는 것은 현명하지 않습니다. 이는 GUI 스레드를 상당한 시간 동안 차단하기 때문입니다. 이로 인해 프레임이 건너뛰거나 애니메이션이 끊길 수 있으며, 이는 어떤 대가를 치르더라도 피해야 합니다.
가비지 컬렉터를 수동으로 호출해도 괜찮은 경우도 일부 있지만(이에 대해서는 다음 섹션에서 더 자세히 설명하겠습니다), 대부분의 경우 가비지 컬렉터를 호출하는 것은 불필요할 뿐만 아니라 오히려 역효과를 낼 수 있습니다.
동일한 암시적 유형을 여러 개 정의하지 마십시오
QML 요소에 QML에서 정의된 사용자 정의 속성이 있는 경우, 해당 요소는 자체 암시적 유형이 됩니다. 이에 대해서는 뒤따르는 섹션에서 더 자세히 설명합니다. ` Component`에 동일한 암시적 유형이 여러 개 정의되어 있으면 메모리가 낭비됩니다. 이러한 상황에서는 일반적으로 재사용할 수 있는 새로운 컴포넌트를 명시적으로 정의하는 것이 더 좋습니다. 이 경우 component 키워드를 사용하여 인라인 컴포넌트를 정의하는 것을 고려해 보십시오.
사용자 정의 속성을 정의하는 것은 종종 성능 최적화에 도움이 될 수 있으며(예를 들어, 필요하거나 재평가되는 바인딩의 수를 줄이기 위해), 컴포넌트의 모듈성과 유지 관리성을 향상시킬 수도 있습니다. 이러한 경우 사용자 정의 속성을 사용하는 것이 권장됩니다. 그러나 새로운 유형을 두 번 이상 사용하는 경우, 메모리 절약을 위해 이를 별도의 컴포넌트(인라인 또는 .qml 파일)로 분리해야 합니다.
기존 컴포넌트 재사용
새로운 컴포넌트를 정의할 계획이라면, 해당 플랫폼의 컴포넌트 세트에 이미 동일한 컴포넌트가 존재하지 않는지 다시 한 번 확인해 볼 가치가 있습니다. 그렇지 않으면, QML 엔진이 본질적으로 기존에 존재하며 이미 로드되었을 가능성이 있는 다른 컴포넌트와 중복되는 타입에 대한 타입 데이터를 생성하고 저장하도록 강요하게 됩니다.
pragma library 스크립트 대신 싱글톤 타입 사용
프래그마 라이브러리 스크립트를 사용하여 애플리케이션 전체의 인스턴스 데이터를 저장하고 있다면, 대신 ` QObject ` 싱글톤 타입을 사용하는 것을 고려해 보십시오. 이렇게 하면 성능이 향상되고, JavaScript 힙 메모리 사용량이 줄어듭니다.
QML 애플리케이션의 메모리 할당
QML 애플리케이션의 메모리 사용량은 네이티브 힙 사용량과 자바스크립트 힙 사용량, 두 부분으로 나눌 수 있습니다. 각 부분에서 할당되는 메모리 중 일부는 QML 엔진이나 자바스크립트 엔진에 의해 할당되므로 피할 수 없지만, 나머지는 애플리케이션 개발자의 결정에 따라 달라집니다.
네이티브 힙에는 다음이 포함됩니다:
- QML 엔진의 고정적이고 피할 수 없는 오버헤드(구현 데이터 구조, 컨텍스트 정보 등);
- 애플리케이션이 어떤 모듈과 컴포넌트를 로드했는지에 따라 QML 엔진이 생성하거나 디스크 캐시에서 불러오는, 유형별 속성 메타데이터를 포함한 컴포넌트별 컴파일된 데이터 및 유형 정보;
- 애플리케이션이 인스턴스화하는 컴포넌트에 따라, 객체별 C++ 데이터(속성 값 포함)와 요소별 메타객체 계층 구조;
- QML 임포트(라이브러리)에 의해 특별히 할당된 모든 데이터.
JavaScript 힙에는 다음이 포함됩니다:
- JavaScript 엔진 자체의 고정적이고 피할 수 없는 오버헤드(내장 JavaScript 유형 포함);
- 자바스크립트 통합에 따른 고정적이고 피할 수 없는 오버헤드(로드된 타입에 대한 생성자 함수, 함수 템플릿 등);
- 각 유형에 대해 런타임 시 JavaScript 엔진에 의해 생성되는 유형별 레이아웃 정보 및 기타 내부 유형 데이터(유형에 관한 내용은 아래 참고 사항 참조);
- 객체별 자바스크립트 데이터("var" 속성, 자바스크립트 함수 및 신호 핸들러, 최적화되지 않은 바인딩 표현식);
- 식 평가 중에 할당된 변수.
또한, 메인 스레드에서 사용하기 위해 할당된 하나의 자바스크립트 힙과, 선택적으로 WorkerScript 스레드에서 사용하기 위해 할당된 또 다른 자바스크립트 힙이 하나씩 존재합니다. 애플리케이션이 WorkerScript 요소를 사용하지 않는다면, 해당 오버헤드는 발생하지 않습니다. 자바스크립트 힙의 크기는 수 메가바이트에 달할 수 있으므로, 메모리 제약이 있는 기기를 위해 작성된 애플리케이션의 경우 ` WorkerScript ` 요소를 사용하지 않는 것이 가장 좋습니다.
QML 엔진과 자바스크립트 엔진 모두 관찰된 유형에 대한 유형 데이터 캐시를 자동으로 생성한다는 점에 유의하십시오. 애플리케이션에 의해 로드되는 모든 컴포넌트는 별개의(명시적) 유형이며, QML에서 자체 사용자 정의 속성을 정의하는 모든 요소(컴포넌트 인스턴스)는 암시적 유형입니다. 사용자 정의 속성을 전혀 정의하지 않은 요소(컴포넌트의 인스턴스)는 JavaScript 및 QML 엔진에 의해 자체 암시적 유형이 아닌, 컴포넌트에 의해 명시적으로 정의된 유형으로 간주됩니다.
다음 예제를 살펴보겠습니다.
import QtQuick
Item {
id: root
Rectangle {
id: r0
color: "red"
}
Rectangle {
id: r1
color: "blue"
width: 50
}
Rectangle {
id: r2
property int customProperty: 5
}
Rectangle {
id: r3
property string customProperty: "hello"
}
Rectangle {
id: r4
property string customProperty: "hello"
}
}이전 예제에서, 직사각형 r0 및 r1 에는 사용자 정의 속성이 없으므로, JavaScript 및 QML 엔진은 이 둘을 모두 동일한 유형으로 간주합니다. 즉, r0 및 r1 은 모두 명시적으로 정의된 Rectangle 유형으로 간주됩니다. 직사각형 r2, r3 및 r4 은 각각 사용자 정의 속성을 가지고 있으며, 각각 서로 다른(암시적) 유형으로 간주됩니다. r3 와 r4 는 속성 정보가 동일함에도 불구하고, 단순히 이들이 인스턴스인 컴포넌트에서 사용자 정의 속성이 선언되지 않았다는 이유만으로 서로 다른 유형으로 간주된다는 점에 유의하십시오.
r3 와 r4 가 모두 RectangleWithString 컴포넌트의 인스턴스이고, 해당 컴포넌트 정의에 customProperty 라는 문자열 속성의 선언이 포함되어 있다면, r3 와 r4 는 동일한 유형으로 간주됩니다(즉, 자체 암시적 유형을 정의하는 대신 RectangleWithString 유형의 인스턴스가 됩니다).
메모리 할당에 대한 심층적 고려 사항
메모리 할당이나 성능 상의 절충점에 관한 결정을 내릴 때는 CPU 캐시 성능, 운영 체제의 페이징, 자바스크립트 엔진의 가비지 컬렉션이 미치는 영향을 염두에 두는 것이 중요합니다. 최적의 해결책을 선택하기 위해서는 잠재적인 해결책들에 대해 신중하게 벤치마킹을 수행해야 합니다.
어떤 일반적인 지침도 컴퓨터 과학의 기본 원리에 대한 탄탄한 이해와, 애플리케이션 개발자가 개발 중인 플랫폼의 구현 세부 사항에 대한 실무 지식을 대체할 수는 없습니다. 또한, 상충 관계에 대한 결정을 내릴 때 아무리 많은 이론적 계산도 훌륭한 벤치마크와 분석 도구를 대체할 수는 없습니다.
파편화
단편화는 C++ 개발 시 발생하는 문제입니다. 애플리케이션 개발자가 C++ 타입이나 플러그인을 정의하지 않는다면, 이 섹션을 무시해도 무방합니다.
시간이 지남에 따라 애플리케이션은 대량의 메모리를 할당하고, 해당 메모리에 데이터를 기록한 뒤, 일부 데이터 사용이 완료되면 메모리의 일부를 해제합니다. 이로 인해 “사용 가능한” 메모리가 비연속적인 블록으로 분산될 수 있으며, 이 메모리는 운영 체제에 반환되어 다른 애플리케이션이 사용할 수 없게 됩니다. 또한 ‘활성’ 데이터가 물리적 메모리의 여러 페이지에 흩어져 있을 수 있으므로, 애플리케이션의 캐싱 및 액세스 특성에도 영향을 미칩니다. 이는 결과적으로 운영 체제가 스왑을 수행하도록 강요할 수 있으며, 이로 인해 파일 시스템 I/O가 발생할 수 있는데, 이는 상대적으로 매우 느린 작업입니다.
조각화는 풀 할당기(및 기타 연속 메모리 할당기)를 활용하거나, 객체의 수명을 신중하게 관리하여 한 번에 할당되는 메모리 양을 줄이거나, 캐시를 주기적으로 정리하고 재구축하거나, 가비지 컬렉션이 포함된 메모리 관리형 런타임(예: 자바스크립트)을 활용함으로써 방지할 수 있습니다.
가비지 컬렉션
자바스크립트는 가비지 컬렉션을 제공합니다. 네이티브 힙이 아닌 자바스크립트 힙에 할당된 메모리는 자바스크립트 엔진이 소유합니다. 엔진은 주기적으로 자바스크립트 힙에 있는 참조되지 않은 모든 데이터를 수거합니다.
가비지 컬렉션의 시사점
가비지 컬렉션에는 장단점이 있습니다. 이는 객체 수명을 수동으로 관리하는 것이 덜 중요해짐을 의미합니다. 그러나 동시에, 잠재적으로 오랜 시간이 소요될 수 있는 작업이 애플리케이션 개발자가 통제할 수 없는 시점에 자바스크립트 엔진에 의해 시작될 수도 있음을 의미합니다. 애플리케이션 개발자가 자바스크립트 힙 사용량을 신중하게 고려하지 않는다면, 가비지 컬렉션의 빈도와 지속 시간이 애플리케이션 사용 경험에 부정적인 영향을 미칠 수 있습니다. Qt 6.8부터는 가비지 컬렉터가 증분 방식으로 작동하므로, 작업 시간은 짧아지지만 중단 횟수는 더 많아질 수 있습니다.
가비지 컬렉터 수동 호출
QML로 작성된 애플리케이션은 (대부분의 경우) 특정 시점에서 가비지 컬렉션이 수행되어야 합니다. 가비지 컬렉션은 자바스크립트 엔진이 자체 일정에 따라 자동으로 트리거되지만, 경우에 따라 애플리케이션 개발자가 가비지 컬렉터를 수동으로 호출할 시점을 결정하는 것이 더 나을 수도 있습니다(비록 일반적으로는 그렇지 않지만).
애플리케이션 개발자는 애플리케이션이 상당한 시간 동안 유휴 상태가 될 시점을 가장 잘 파악하고 있을 가능성이 높습니다. QML 애플리케이션이 많은 양의 자바스크립트 힙 메모리를 사용하여, 특히 성능에 민감한 작업(예: 목록 스크롤, 애니메이션 등) 중에 정기적이고 방해가 되는 가비지 컬렉션 주기가 발생하는 경우, 애플리케이션 개발자는 활동이 전혀 없는 기간 동안 가비지 컬렉터를 수동으로 호출하는 것이 현명할 수 있습니다. 유휴 기간은 가비지 컬렉션을 수행하기에 이상적입니다. 활동 중에 가비지 컬렉터를 호출할 경우 발생할 수 있는 사용자 경험 저하(프레임 생략, 끊기는 애니메이션 등)를 사용자가 전혀 눈치채지 못하기 때문입니다.
가비지 컬렉터는 자바스크립트 내에서 ` gc() `를 호출하여 수동으로 실행할 수 있습니다. 이렇게 하면 증분 방식이 아닌 전체 가비지 컬렉션 주기가 수행되는데, 완료하는 데 수백 밀리초에서 천 밀리초 이상 걸릴 수 있으므로 가능한 한 피해야 합니다.
메모리 대 성능의 상충 관계
어떤 상황에서는 메모리 사용량을 늘리는 대신 처리 시간을 단축하는 절충안을 선택할 수 있습니다. 예를 들어, 밀집된 루프에서 사용되는 심볼 조회 결과를 자바스크립트 표현식 내의 임시 변수에 캐싱하면 해당 표현식을 평가할 때 성능이 크게 향상되지만, 임시 변수를 할당해야 하는 단점이 있습니다. 어떤 경우에는 이러한 절충이 합리적일 수 있지만(위 사례와 같이 거의 항상 합리적인 경우), 다른 경우에는 시스템의 메모리 부하를 증가시키지 않기 위해 처리 시간이 약간 더 걸리더라도 이를 허용하는 편이 더 나을 수 있습니다.
경우에 따라 메모리 부하 증가의 영향은 극심할 수 있습니다. 어떤 상황에서는 예상되는 성능 향상을 위해 메모리 사용량을 희생하는 것이 페이지 스래시(page-thrash)나 캐시 스래시(cache-thrash)를 증가시켜 성능을 크게 저하시킬 수 있습니다. 주어진 상황에서 어떤 해결책이 최선인지 판단하기 위해서는 항상 상충 관계의 영향을 신중하게 벤치마킹해야 합니다.
캐시 성능 및 메모리-시간 상의 상충 관계에 대한 심층적인 정보는 다음 문서를 참조하십시오.
- Ulrich Drepper의 훌륭한 기사: "모든 프로그래머가 메모리에 대해 알아야 할 사항", 주소: https://people.freebsd.org/~lstewart/articles/cpumemory.pdf.
- C++ 애플리케이션 최적화에 관한 아그너 포그(Agner Fog)의 훌륭한 매뉴얼: http://www.agner.org/optimize/.
빠른 부팅 및 시작 최적화
Qt Quick 애플리케이션의 빠른 부팅을 위해 최적화한 실제 경험을 바탕으로, 다음 모범 사례를 고려해 보십시오:
- 애플리케이션을 처음부터 빠르게 시작되도록 설계하십시오. 사용자가 가장 먼저 무엇을 보게 할지 고려하십시오.
- 다음 QML Profiler 를 사용하여 시작 시 병목 현상을 파악하십시오.
- 체인 로딩을 사용하십시오. CPU 코어 수만큼만 loaders 를 실행하십시오(예: 코어 2개인 경우, 두 개의 로더가 동시에 실행됨).
- 일부 콘텐츠가 즉시 표시될 수 있도록 첫 번째 로더( loader )는 비동기 방식으로 실행해서는 안 됩니다. 그 후에 비동기 로더를 트리거하십시오.
- 필요한 경우에만 백엔드 서비스에 연결하십시오.
- 필요할 때만 임포트되는 QML 모듈을 생성하세요. 지연 로딩되는 모듈과 타입을 사용하면, 애플리케이션에서 필요에 따라 비핵심 서비스를 이용할 수 있게 할 수 있습니다.
- optipng와 같은 도구를 사용하여 PNG/JPG 이미지를 최적화하십시오.
- 정점 수를 줄이고 보이지 않는 부분을 제거하여 3D 모델을 최적화하십시오.
- glTF를 사용하여 3D 모델 로딩을 최적화하세요.
- clip 및 opacity는 성능에 영향을 줄 수 있으므로 사용을 제한하십시오.
- GPU의 한계를 측정하고 UI를 설계할 때 이를 고려하십시오. 자세한 내용은 Frame Captures and Performance Profiling 를 참조하십시오.
- QML 파일을 사전 컴파일하려면 Qt Quick Compiler 를 사용하여 QML 파일을 사전 컴파일하십시오.
- 사용 중인 아키텍처에서 정적 링크가 가능한지 확인하십시오.
- 명령형 시그널 핸들러 대신 선언형 바인딩을 사용하도록 노력하십시오.
- 속성 바인딩은 단순하게 유지하십시오. 일반적으로 QML 코드는 단순하고, 재미있으며, 가독성이 좋도록 작성하십시오. 그러면 성능도 자연스럽게 향상됩니다.
- 생성 시간이 문제라면 복잡한 컨트롤을 이미지나 셰이더로 대체하십시오.
다음은 피해야 할 사항입니다:
- QML을 지나치게 남용하지 마십시오. QML을 사용한다고 해도, 모든 작업을 반드시 QML로 처리할 필요는 없습니다.
- main.cpp에서 모든 것을 초기화하지 마십시오.
- 필요한 모든 인터페이스를 포함하는 거대한 싱글톤을 만들지 마십시오.
- ListView 나 다른 뷰를 위한 복잡한 델리게이트를 생성하지 마십시오.
- 꼭 필요한 경우가 아니면 clip을 사용하지 마십시오.
- 로더(Loaders)를 과도하게 사용하는 흔한 함정에 빠지지 마세요. Loader 는 애플리케이션 페이지와 같은 대용량 요소를 지연 로딩하는 데는 훌륭하지만, 단순한 요소를 로딩할 때는 너무 많은 오버헤드를 유발합니다. 이는 모든 것을 가속화하는 마법 같은 도구가 아닙니다. 단지 추가적인 QML 컨텍스트를 가진 또 하나의 요소일 뿐입니다.
이러한 관행을 따르면, 특히 임베디드 기기에서 1초 미만의 시작 시간과 원활한 사용자 경험을 달성하는 데 도움이 됩니다.
© 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.