음악, 삶, 개발
ValueTree 공부하기 파트1 : 이론 본문
< 참고자료 >
ValueTree::Listener Class Reference
< 들어가기 앞서.. >
이 기본기 포스트는, Tutorial: The ValueTree class 를 바탕으로 쓰여졌다.
따라서 내 포스팅을 다 읽고도, 또는 시간이 흘러 무언가 희미하다면..
반드시 위의 Juce 공식 튜토리얼을 읽고 이 포스팅을 다시 읽도록한다.
< ValueTree 란 ? >
ValueTree 는 Juce 에서 제공하는 컨테이너이다.
Thread 안정성을 보장하지 않기때문에, GUI Thread 에서만 사용하는것이 좋다.
여러 Component 가 하나의 데이터 Model 을 공유해야할때 유용하다.
ValueTree 의 값이 변하면, ValueTree Listener 를 통해 값의 변화를 Notify 해줄수있다.
< Node, Type, PropetyName, Property 의 용어 이해하기 >
ValueTree 를 Node 라고도 한다.
A란 ValueTree 안에 B 라는 ValueTree 가 있다면,
A는 부모 Node, B 를 자식 Node 라고 한다.
Node 의 이름을 Type 이라고 하며,
각 Node 안에 key 와 value 들이 연속적으로 자리하게되는데,
이 key 를 PropertyName, value 를 Property 라고한다.
<Parent Name = "Lee" Age = 30 /> // This is Node
위에 전체 한줄이 ValueTree, 즉 Node 이다.
Parent 가 이 Node 의 Type 이다.
Name 은 PropertyName 이고, "Lee" 는 Property 이다.
Age 가 PropertyName 이고, 30 이 Property 이다.
만약 Age 의 값인 30 이, 40 으로 변경되었다면
"Property 가 변경되었다" = valueTreePropertyChanged 라고 말한다.
< ValueTree 의 생김새 : XML 같지만, value 가 실제 Type >
XML 포멧과 매우 비슷하다.
부모 property, 그 하위로 child property 가 계속해서 추가 될수있다.
propety 와 value 를 묶어서 Node 라고 부른다.
또한 XML 파일로 ValueTree 를 Export 할수있다. (어떻게 하는지는 나중에 설명)
하지만 XML 과 가장 큰 차이점이 있다.
XML 의 property 의 value 는 string 인 반면,
ValueTree 의 value 는 실제 type 이다.
예를 들어, 값이 2.4 면 double 이고 false 는 bool 이다.
Max 의 [dict] 와 매우 비슷하다.
< Type 과 Property 를 나타내는 클래스 : juce::Identifier, juce::var >
Type - juce::Identifier
Property - juce::var
위의 두 클래스의 객체를 각 인자에 넘김으로써, 원하는 Property 를 추가하거나, 변경할수있다.
< ValueTree 의 Property 가 되는 Var 클래스 >
ValueTree 의 모든 Property 는 다양한 Type 을 나타낼수있는 Var 클래스의 객체이다.
이 점이 모든 값을 텍스트로 표현하는 XML 과 가장 큰 차이인것이다.
다양한 데이터 타입을 ValueTree 에서 값으로 표현하기위해,
Var 클래스를 사용하여 각 Type 들을 Wrap 해준것이다.
위의 인자에 나타내는 Type 들이, ValueTree 에서 우리가 Property 로 표현할수있는것들이다.
따라서, ValueTree 를 이해하기위해 Var 클래스를 이해하는것이 필수적이다.
약간 JavaSript 의 var 유형을 떠올려볼수있겠다.
< ValueTree 디버깅 : ValueTree 를 XML 파일로 Export 하기 createXml() >
Max 에서는 dict 를 콘솔로 찍어볼수있지만,
C++ 환경에서는 어려운일이다.
따라서 ValueTree 를 XML 파일로 Export 하여 디버깅을해야한다.
void exportValueTree(const juce::ValueTree& tree) {
tree.createXml()->writeTo({"/Users/leestrument/Desktop/tree.xml"});
}
ValueTree 의 createXml() 은 XmlElement 객체의 unique_ptr 를 return 한다.
이 포인터로 -> 사용하여 writeTo({"파일경로"}) 를 호출하면, 최종적으로 XML 파일을 생성할수있다.
void exportValueTree(const juce::ValueTree& tree) {
tree.createXml()->writeTo({"/Users/leestrument/Desktop/tree.xml"});
}
int main () {
juce::ValueTree tree {"LEESTRUMENT"};
exportValueTree(tree);
return 0;
}
< ValueTree 생성하기 >
juce::ValueTree tree {"Parent"};
위와 같이, ValueTree 객체를 생성할때 인자로 넣는 이름이
ValueTree 가장 Top Node 의 Type이 된다.
이때 한 가지 중요한 사실이 있는데,
Type명에 빈칸이 있으면 컴파일이 되지않는다.
예를 들어 "Type Name" 이라고 하면 안되고, "TypeName" 이라고 해야한다.
< 반드시 ValueTree 명을 제공하자 >
juce::ValueTree tree;
위와 같이 ValueTree 객체를 생성할수있지만, 좋은 생각이 아니다.
Juce 공식 문서에 의하면 이렇게 이름이없는 ValueTree 는,
내가 어떠한 ValueTree 의 멤버함수를 호출해도 ValueTree 에 아무 변화도 일어나지않는다고한다.
결국 이름이 없는 ValueTree 는 Invalid 상태라고 한다.
이를 ValueTree.isValid() 로 확인할수있다.
또한 나중에 보겠지만, ValueTree 안에 ValueTree 가 삽입될수있다.
이때 분명히 이 ValueTree 에 내가 정해준 이름을 사용하여
특정 Tree 를 get 해야하는 상황이 자주 발생하게된다.
따라서 반드시 ValueTree 를 생성할때 이름을 주도록 해야한다.
주의할점은 이름에 빈칸이 없어야한다.
juce::ValueTree tree {"MyTreeName"}; // 이름명은 빈칸없이!
< 매우 중요한 ValueTree 의 특징 1 : 복사해도 복사되지않는다 >
아래는 Juce 공식 문서에 적혀있는 설명이다.
" The data itself is actually stored in a hidden shared instance,
for which the ValueTree class is just a light, reference-keeping wrapper interface.
You can pass them around quickly by value, and not have to use pointers directly;
returning a ValueTree object from a function does not copy any data,
only a reference, so it makes your interfaces both simple and safe.
You never have to worry about deleting them yourself;
as soon as you are no longer using one anywhere,
it will be automatically destroyed.
This also means that the node data will never be accidentally deleted whilst you might be still using it,
which is particularly handy for ensuring an asynchronous UI will never encounter dangling pointers. "
중요한 부분을 정리하면 아래와 같다.
1. ValueTree 데이터는 숨겨진 공유된 인스턴스에 저장된다.
2. 값으로 넘겨도 (pass-by-value), 값으로 return 해도 모두 reference 이며 데이터가 복사되지않는다.
따라서 pointer 로 ValueTree 를 넘기려한다거나 하는 멍청한 행동을 하지말아야한다.
그냥 Value 로 어디든 Pass 하고 Return 하면 된다.
이러한 특징이 여러 Componet 에서 하나의 데이터를 공유하게해주는것이다.
복사해도 복사되지않는다에 첨언하며 중요한 개념이있다.
동일한 데이터를 두개의 ValueTree 가 가리킬수있다.
아래와 같은 경우를 보자.
juce::ValueTree myNode (myNodeType); // creates a new node of type "MyNode"
juce::ValueTree sameNode (myNode); // This object points to the same data as myNode
이 경우 sameNode 는 myNode 가 가리키는 동일한 데이터를 나타낸다고한다.
한마디로 데이터가 복사되지않는다는것이다.
만약 sameNode 에 변화를 가하면, myNode 에도 변화가 일어나는것이다. (이 둘은 동일한 데이터를 가리키므로)
JavaScript 에서 Object 를 복사해도 Reference 로 복사되는것과 같은 이치이다.
그럼 아래와 같이 assignment 하게되면 어떻게 될까?
juce::ValueTree myNode (myNodeType); // creates a new node of type "MyNode"
juce::ValueTree sameNode (myNode); // This object points to the same data as myNode
juce::ValueTree otherNode (myNodeType); // This creates a second (new) "MyNode" node...
otherNode = sameNode; // ...but the object now points to the first instance
위의 경우 역시 otherNode 가 가리키는 데이터가 sameNode 가 가리키는 데이터로 변경된다고한다.
(포인터처럼 가리키는 데이터의 변경이 가능하다는것을 의미한다)
결과적으로 myNode, sameNode, otherNode 는 모두 동일한 데이터를 가리키게된다.
< 매우 중요한 ValueTree 의 특징 2 : ValueTree 안에 ValueTree >
ValueTree 안에 또다른 ValueTree 가, 이안에 다시 또다른 ValueTree가 삽입될수있다.
깊이의 제한은 없다.
ValueTree 가 제공하는 함수중 다음과 같은 함수가 있다.
void addChild (const ValueTree &child, int index, UndoManager *undoManager);
원하는 위치에 다른 ValueTree 를 삽입할수있는것이다.
< 매우 중요한 ValueTree 의 특징 3 : 알림 서비스 (Notification) >
Juce 공식 문서에 아래와 같이 나와있다.
Notifications
Another very powerful feature is the ability for ValueTrees to send notifications when their contents change;
this offers huge practical simplifications, particularly in keeping the UI up to date.
For example, a Component object being used to display the contents of a node can simply refresh itself
whenever it sees that the data has been edited—you need only implement it as a ValueTree::Listener subclass.
ValueTree 를 공유하는 객체들간에 알림서비스를 해준다는것이 매우 중요한 기능이다.
예를 들어, A 라는 객체가 ValueTree 를 변경하였는데,
이 변경을 B 라는 객체가 이 알림을 받아, GUI 를 다시 그린다던지 하는 작업이 가능한것이다.
우리가 Youtube 의 KBS 날씨 뉴스의 구독자인데,
내일은 날씨가 춥다라는 알림을 받았고, "외출하지말자" 라는 콜백을 스스로 정의한것이다.
< ValueTree::Listener : 구독, 알림 서비스 사용하기 >
우리가 Youtube 에서 다른 사람의 채널을 구독, 알림받는것과 같다고 보면 된다.
ValueTree 를 구독하고 알림서비스 받기위해서는 몇가지 4단계를 거쳐야한다.
1. juce::ValueTree::Listener 를 상속받는 클래스를 생성한다.
2. Constructor 에서 ValueTree 객체를 넘겨받아, 멤버 변수로 저장한다. (값으로 넘겨받아도, 복사되지않으니 걱정말자!)
3. 구독하기 : Constructor 에서 addListener(this) 를 호출한다.
4. 알림서비스 : ValueTree::Listener 가 제공하는 콜백함수를 override 한다.
struct Lee : public juce::ValueTree::Listener {
Lee(const juce::ValueTree& v)
: tree(v)
{
tree.addListener(this); // ValueTree 구독
}
// ValueTree 알림
valueTreePropertyChanged (juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) override {
DBG("ValueTree 가 변경되었습니다!");
}
juce::ValueTree tree;
};
위의 코드를 작성하면, ValueTree 의 구독자 즉, Listener 가 될수있는것이다.
< ValueTree::Listener 가 제공하는 6가지 알림서비스 >
ValueTree 는 6가지 알림서비스를 제공한다.
1. valueTreePropertyChanged
2. valueTreeChildAdded
3. vaueTreeChildRemoved
4. valueTreeChildOrderChanged
5. valueTreeParentChanged
6. valueTreeRedirected
아래는 valueTreePropertyChanged 의 공식문서 내용이다.
This method is called when a property of this tree (or of one of its sub-trees) is changed.
Note that when you register a listener to a tree, it will receive this callback for property changes in that tree,
and also for any of its children, (recursively, at any depth).
If your tree has sub-trees but you only want to know about changes to the top level tree,
simply check the tree parameter in this callback to make sure it's the tree you're interested in.
어떠한 깊이의 sub tree 에서든 변화가 일어나면 이 함수가 호출된다는 내용이다.
따라서 이 변화가 내가 생각하는 변화가 맞는지 확인하려면
첫번째 인자인 ValueTree& treeWhosePropertyHasChanged 가 어떠한 sub-tree 인지 알려주므로, 이를 확인하도록한다.
< juce::Identifier : ValueTree 의 property명 >
juce::Identifier 는 juce::String 과 같은 string 이다.
이 Identifier 를 property 를 추가하는 함수의 첫번째 인자로 넘겨야한다.
juce::ValueTree& setProperty(const juce::Identifier& name, const juce::var& newValue, UndoManager* undoManager);
결국 string 인데, 왜 juce::String 을 쓰지않을까 의아했는데
공식 문서에 아래와 같이 이유가 적혀있었다.
궁금하면 읽어보라.
Why not just use the String class?
There are two main reasons for using a specialised class instead of the general purpose String class.
- It can force a limited character set: Firstly, this class enforces a limitation on the characters which can make up a valid key; it only allows alphanumeric characters and special characters _-:#$%. This might sound a bit rubbish, but it makes it possible to ensure compatibility with other systems with the same limitation (for example, the XML format and scripts).
- It can be optimised for purpose: The second (but most important) reason stems from how we want to use them. They are to act as a key to identify a single item from a list of any size, and so the most common operation to be performed with them is a comparison. However, comparisons of String objects can be fairly slow. We actually have to check the text, and only when we find the first different character can we say for sure that two String objects are not the same. For strings that are mostly the same, we may end up checking most of the letters (and strings which are a match will have had all of their characters tested). For a single comparison, that might be acceptable, but when you want to compare one string against a list (as we would when using it as a key), the whole business can take a long time.
< juce::Identifier 객체는 미리 만들어놓자 : 리터럴로 넘기지마! >
How to minimise costs
A good strategy is to initialise some easily-accessible Identifier instances at startup. From your code, you can then use these instead of literal strings, and you will never incur any further lookup penalties from them. You could put them in a globally accessible namespace, use file static instances, or even static class members to help organise them.
juce::Identifier 를 run-time 에 리터럴로 즉시 만드는것은 cost 가 있다고한다.
따라서 미리 이 Identifier 들을 생성해놓고, 객체로 넘기는 형식이 좋다고 한다.
우리가 어떠한 property 명을 사용할지는 당연히 미리 정할것이고,
이들을 한 공간에 미리 정의해놓는것이 현명할것이다.
아래는 공식문서의 예제 코드이다.
static juce::Identifier myNodeType ("MyNode"); // pre-create an Identifier
juce::ValueTree myNode (myNodeType); // This is a valid node, of type "MyNode"
바로 리터럴을 넘겨도되지만, 왠만하면 그렇게 하지말라고한다.
아래는 공식 설명의 일부.
You can use a string literal instead of explicitly providing an Identifier object,
but it is better practice to use an existing Identifier instance where possible.
< ValueTree::getType() : ValueTree 의 Identifier 가져오기 >
위에서, 알림서비스 콜백을 받을때 우리가 기대한 ValueTree 가 맞는지 확인해야한다고 했었다.
또한 이러한 확인을 편리하게 하기위해 미리 juce::Identifier 객체들을 만들어놓아야한다.
ValueTree 의 juce::Identifier 객체를 return 해주는것이 getType() 멤버 함수이다.
아래는 현재 ValueTree 가 내가 생각하는 ValueTree 가 맞는지,
juce::Identifier 로 확인하는 코드이다.
static juce::Identifier myNodeType ("MyNode"); // pre-create an Identifier
juce::ValueTree myNode (myNodeType); // This is a valid node, of type "MyNode"
void foo (const juce::ValueTree& someNode) {
if (someNode.getType() == myNodeType) {
// This would be hit for nodes created as “MyNode”
}
}
getType() 으로 return 한 Identifier 와 내가 만들어놓은 Identifier 로 if 문안에서 비교를 하고있다.
< 초기화후 변경될수없는 Type : juce::Identifier >
Type은 한번 등록되면 변경할수없다.