콘텐츠로 이동

Extend - 확장과 재사용

Extend 란?

Quad 에서는 로블록스의 Frame 이나 TextButton 같은 기본 오브젝트만 만들 수 있는것이 아닙니다. Quad 는 자신만의 버튼, 혹은 여러 컴포넌트들을 추상적으로 만들고. 다시 활용할 수 있도록 Extend 를 지원합니다.

ExtendRegister 와 더불어 Quad 의 가장 중요한 기능으로써, 다음과 같은 장점을 지닙니다.

  1. 재사용 가능합니다. 나만의 버튼을 만들고, 고치고 싶은 마음이 생겨 고치면 자동으로 사용된 곳들이 업데이트됩니다.
  2. 긴 코드를 나누어 코드마다 확실한 목적을 부여해 더 명확한 코드 작성이 가능해집니다.
  3. 자신만의 프로퍼티를 만들거나 완전히 원하는대로 오브젝트를 커스터마이징 할 수 있습니다.
  4. 함수나 생성기를 직접 만드는 것 보다 일관된 방법을 제공합니다.
  5. 매우 유연합니다. 다양한 방법의 구현법을 원하는대로 사용할 수 있습니다.

나만의 클래스를 만들어보기

Extend 객체는 다음과 같이 생성합니다.

1
2
3
4
5
local Quad = require(path.to.module).Init()
local Class = Quad.Class

-- extend 객체를 만듭니다
local myClass = Class.Extend()
일반적으로 Extend 는 한 모듈에 하나씩 넣습니다.

다음과 같은 코드 구조를 만들어보세요. 아래의 코드는 모든 Extend 문법을 설명합니다.

1
2
3
 - ScreenGUI
 |-- localscript
    |-- myClass (ModuleScript)

Extend 클래스를 불러오고 사용합니다
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
local ScreenGUI = script.Parent
local Quad = require(path.to.module).Init()
local Mount = Quad.Mount
local Class = Quad.Class
local Store = Quad.Store

local myStore = Store.GetStore("myStore")
myStore.bgColor = Color3.fromRGB(0,0,0)

-- Class(require(script.myClass)) 와 같습니다.
-- 줄여서 Class(script.myClass) 로 쓸 수도 있습니다.
local myClass = Class(script.myClass)

-- 프레임에 넣거나 하여도 무방합니다
-- 일반 오브젝트와 똑같이 id 를 넣고 GetObject 로
-- 가져올 수도 있습니다
local main = myClass "main" {
    -- 이 테이블은 모듈의 :Init(props) 에 제공됩니다.
    -- :Render(props) 에도 이 테이블이 제공됩니다.
    Testing = 1;
    Text = "Test";
    -- 레지스터 값을 넘겨줄 수도 있습니다.
    -- 받는쪽도 레지스터인 경우 값이 연동됩니다.
    BackgroundColor3 = myStore "bgColor";
}

Mount(ScreenGUI, main)
print(Store.GetObject("main") == main) -- true

-- 이 값을 바꾸면 연동된 BackgroundColor3 도 바뀔것입니다
myStore.bgColor = Color3.fromRGB(255,255,255)

-- 값 변경됨을 연결하기
main
    :GetPropertyChangedSignal("AbsolutePosition")
    :Connect(function()
        print("AbsolutePosition",main.AbsolutePosition)
    end)

-- main:Update()
-- 강제로 다시 Render 를 실행하도록 만듭니다.

-- main.Testing = 2
-- 업데이트 트리거이므로 이 값을 변경하면.
-- main:Update() 와 같은 효과가 납니다.
Extend 클래스를 만듭니다. 다른곳에서 불러서 사용할 수 있습니다.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
local Quad = require(path.to.module).Init()
local Class = Quad.Class
local Event = Quad.Event

local TextLabel = Class "TextLabel"

local myClass = Class.Extend()

-- Init 는 Render 전 props 를 초기화 하는 용도로 사용합니다
-- Init 에는 프로퍼티 리스트가 제공됩니다. Init 는 없더라도 무방합니다.
function myClass:Init(props)
    -- 생성된 오브젝트에 프라이빗 값을 만듭니다.
    -- 이름 앞에 _ 가 붇으면 외부에서 사용되는 목적이 아닌
    -- 내부에서 사용되는 목적으로만 사용할 수 있습니다
    -- 이 경우 안전성을 위해 register 를 사용할 수
    -- 없습니다.
    self._testValue = 2

    -- 프로퍼티의 기본값을 정해줄 수 있습니다.
    -- 생성 시 값이 누락된 경우 사용할 값을 정해줄 수 있습니다
    -- 키, 값 으로 구성합니다
    props:Default("Text",   "Testing Label")
    props:Default("ZIndex", 1)
end

-- myClass 가 진짜 그려지는 부분입니다.
-- Render 도 Init 처럼 프로퍼티 리스트가 제공됩니다.
function myClass:Render(props)
    return TextLabel {
        Size = UDim2.fromOffset(200,200);
        -- self 에 _label 로 이 TextLabel 을 넣습니다.
        -- 자식 오브젝트에도 사용할 수 있는 문법입니다.
        self "_label";

        -- _holder 는 예약된 값입니다. 자식들이 여기에 들어와야
        -- 함을 나타냅니다. 리스트나 그리드가 있는 경우 유용합니다.
        -- 따로 설정하지 않는 경우 Render 가 리턴한 값을 사용합니다.
        self "_holder";

        -- GetPropertyChangedSignal 가 작동할 수 있도록
        -- 프로퍼티 변경 이벤트를 연결해줍니다
        -- 이 또한 자식 오브젝트에도 사용할 수 있는 문법입니다.
        [Event.Prop "AbsolutePosition"] = self "AbsolutePosition";
        -- 마우스 클릭같은 다른 이벤트를 GetPropertyChangedSignal 에
        -- 연결하려면 :EmitPropertyChangedSignal() 를 호출해주면 됩니다.
        [Event "MouseEnter"] = function (this,x,y)
            -- 키, 값이 들어갑니다. 값이 없으면 self[key] 로 대신합니다.
            self:EmitPropertyChangedSignal("Testing2",1)
        end;

        -- 받은 레지스터를 연동합니다.
        -- 만약 props.BackgroundColor3 만 사용하면
        -- 처음에만 레지스터에서 값을 읽어옵니다.
        -- 따라서 값을 변경해도 변경사항이 적용되지 않습니다.
        BackgroundColor3 = props "BackgroundColor3":Tween();

        -- 입력 값과 상관없이 레지스터를 쓸 수 있습니다.
        -- 이 경우 main.Text = "New" 처럼 직접 변경하면
        -- 그 변경이 적용됩니다.
        Text = props "Text";
        ZIndex = props "ZIndex";
        -- ... 자식을 넣어도 무방합니다.
        -- Frame {
        --    [Event.Created] = function(self)
        --        print(self.Parent) -- nil 일 수도 있습니다
        --        따라서 부모를 건들여야 하는 경우, self "" 를 통해
        --        self 값에 넣어놓고 나중에 AfterRender 로 해결해야합니다.
        --    end;
        -- }
    }
end


-- AfterRender 는 Render 직후 실행됩니다.
-- 렌더링 후 실행해야 할 것을 넣을 수 있습니다.
-- object 는 :Render 가 반환한 값입니다.
-- AfterRender 는 없더라도 무방합니다.
function myClass:AfterRender(object)
    print(self._label,"이 생성되었습니다")
    -- 이렇게 하면 레지스터로 연결된 부분도 업데이트됩니다.
    self.ZIndex = 2
end

-- function myClass:Unload(object)
-- :Destroy 가 호출될 때 사용됩니다. 따로 선언하지 않아도
-- 연결을 모두 끊고 없에주지만 필요한 경우 직접 없에줄 수 있습니다
-- object 는 :Render 가 반환한 값입니다

-- AbsolutePosition 값을 반환할 수 있도록 만듭니다
function myClass.Getter.AbsolutePosition(self)
    return self._label.AbsolutePosition
end

-- AbsolutePosition 값을 설정할 수 없도록 만듭니다
function myClass.Setter.AbsolutePosition(self,newValue)
    -- 필요한 경우 값을 설정하도록 만들 수 있습니다
    error("AbsolutePosition 값은 설정할 수 없습니다")
end

-- main.Testing = 2 다음과 같이 값을 변경하면 오브젝트가
-- 완전히 다시 그려집니다. 즉 업데이트 트리거로 사용합니다.
myClass.UpdateTriggers.Testing = true

-- 만든 클래스를 반환해줍시다.
return myClass

Extend

Extend 에서 사용할 수 있는 값들과 메소드입니다.

self(name:string)->linker

Extend 메소드 전역에서 사용가능합니다. 링커를 만들며, PropertyChangedSignal 를 이어주거나, self 에 원하는 오브젝트를 넣는 용도로 사용됩니다. 자세한 정보는 Render 를 확인해주세요.


Overwite REQUIRED :Render(props:valueStore)

화면에 그려지는 UI 를 만들어지게 하는 함수입니다. Extend 가 그려지려면 무조건 있어야합니다.
이 오브젝트를 만드는데 들어간 프로퍼티 목록을 첫번째 인자로 줍니다. 레지스터 문법이 사용될 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
local ScreenGUI = script.Parent
local Quad = require(path.to.module).Init()
local Class = Quad.Class
local Event = Quad.Event
local Mount = Quad.Mount

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {
        [Event "MouseButton1Click"] = function (self,x,y)
            print(("버튼이 %d,%d 에서 눌렸습니다"):format(x,y))
        end;
        Size = props "Size";
        Text = props "Text"; -- 레지스터를 이용합니다
    }
end

local myImported = Class(myClass)
myImported.Size = UDim2.fromOffset(200,200)

local test = myImported {
    Text = "wow"
}
Mount(ScreenGUI, test)

-- 텍스트를 업데이트합니다
while task.wait(1) do
    test.Text = "w"..("o"):rep(#test.Text-1).."w"
end

Overwite Option :Init(props:valueStore)

선택적인 메소드로, Render 가 수행 되기 전에 미리 props 값을 설정할 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local Quad = require(path.to.module).Init()
local Class = Quad.Class

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Init(props)
    -- Size 값이 없다면 기본값을 사용하도록 합니다.
    props:Default("Size", UDim2.fromOffset(100,100))

    -- 앞에 _ 를 붇여 내부 변수를 만듭니다.
    -- 어디에서나 쓰일 수 있으며 register 에 영향주지 않습니다.
    self._asdf = true
end
function myClass:Render(props)
    return TextButton {
        props "Size"
    }
end

local myImported = Class(myClass)
local myButton = myImported() -- 옵션 없음
print(myButton.Size)

Overwite Option :AfterRender(object:any)

선택적인 메소드로, Render 가 수행 된 후 할 작업을 만들 수 있습니다. 첫번째 인자로 Render 가 반환한 값이 주어집니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
local Quad = require(path.to.module).Init()
local Class = Quad.Class

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {
        self "_button"; -- 자기 자신에 이 버튼을 넣습니다
    }
end
function myClass:AfterRender(object)
    print(self._button == object) -- true
end

local myImported = Class(myClass)
myImported() -- 옵션 없음

Overwite Option :Update()

:Render:AfterRender 를 다시 실행하고, 이전에 있던 자식들을 다시 끌어옵니다.
이전의 Render 결과는 제거됩니다

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
local Quad = require(path.to.module).Init()
local Class = Quad.Class

local TextButton = Class "TextButton"
local count = 0

local myClass = Class.Extend()
function myClass:Render(props)
    print("Rendering!")
    count = count + 1
    return TextButton {
        self "_button";
        Name = tostring(count);
    }
end
function myClass:AfterRender(object)
    print(self._button.Name)
end

local myImported = Class(myClass)
local myButton = myImported()
myButton:Update() -- update
myButton:Update() -- update
myButton:Update() -- update


Overwite Option .Getter.<PropertyName>(self)

값을 얻는데 사용됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
local Quad = require(path.to.module).Init()
local Class = Quad.Class

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {}
end
-- 값을 읽는데 사용할 함수를 등록합니다
function myClass.Getter.Test(self)
    return "Hello World"
end

local myImported = Class(myClass)
print(myImported().Test)


Overwite Option .Setter.<PropertyName>(self,newValue:any)

값을 설정하는데 사용됩니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
local Quad = require(path.to.module).Init()
local Class = Quad.Class

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {
        self "_button";
        Text = "wow";
    }
end
-- 값을 지정하는데 사용할 함수를 등록합니다
function myClass.Setter.Test(self,newValue)
    self._button.Text = newValue
end
function myClass.Getter.Test(self)
    return self._button.Text
end

local myImported = Class(myClass)
local myButton = myImported()
print(myButton.Test)
myButton.Test = "Hello World"
print(myButton.Test)


Overwite Option .UpdateTriggers.<PropertyName> = boolean?

해당 값을 변경하면 Update 가 수행될지 여부를 지정합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
local Quad = require(path.to.module).Init()
local Class = Quad.Class

local TextButton = Class "TextButton"
local count = 0

local myClass = Class.Extend()
function myClass:Render(props)
    print("Rendering!")
    count = count + 1
    return TextButton {
        self "_button";
        Name = tostring(count);
    }
end
function myClass:AfterRender(object)
    print(self._button.Name)
end
-- 값 변경시 업데이트 여부를 지정합니다
myClass.UpdateTriggers.Test = true

local myImported = Class(myClass)
local myButton = myImported()
myButton.Test = 1 -- update
myButton.Test = 1 -- update
myButton.Test = 1 -- update


Overwite Option :Unload(object)

:Destroy 가 호출될 때 사용됩니다. 따로 선언하지 않아도 연결을 모두 끊고 없에주지만 필요한 경우 직접 없에줄 수 있습니다. object 는 :Render 가 반환한 값입니다


:GetPropertyChangedSignal(propertyName:string)->Bindable

값 변경을 연결할 수 있는 Bindable을 반환합니다. :EmitPropertyChangedSignal 으로 이 시그널을 작동시킬 수 있습니다. 편의상 이 시그널이 작동할 때, 새로운 값이 첫번째 인자로 주어집니다.


:EmitPropertyChangedSignal(propertyName:string,changedValue:any?)

프로퍼티 변경시 :GetPropertyChangedSignal 에 등록된 함수가 실행되도록 이 함수를 호출해 주어야 합니다. 일반적으로 self.a = 1 과 같은 방식으로 바로 값을 설정하는 경우 자동적으로 이 함수가 실행되지만, 텍스트 박스의 텍스트 처럼 객체의 변경사항을 연동해야 하는 경우 이 함수를 사용할 수 있습니다. changedValue 는 선택사항이며, 주어지지 않으면 self[propertyName]을 반환합니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
local ScreenGUI = script.Parent
local Quad = require(path.to.module).Init()
local Class = Quad.Class
local Mount = Quad.Mount
local Evnet = Quad.Event

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {
        self "_box";
        Text = "test";
        Size = UDim2.fromOffset(200,200);
        [Evnet.Prop "Text"] = function (this,newValue)
            self:EmitPropertyChangedSignal("Text",newValue)
        end;
        -- [Evnet.Prop "Text"] = self "Text" 와 동일합니다.
        -- self(name:string) 문법으로 변경 이벤트 역시 링크
        -- 할 수 있습니다.
    }
end
function myClass.Getter.Text(self)
    return self._box.Text
end

local myImported = Class(myClass)
local myButton = myImported()
myButton:GetPropertyChangedSignal("Text"):Connect(function()
    print(myButton.Text)
end)
Mount(ScreenGUI, myButton)


Event .ChildAdded:Connect((child)->())->Connection

자식 오브젝트가 추가될 때 발생하는 이벤트입니다. :Init 또는 :AfterRender 에서 사용하면 옵션으로 넣은 자식 오브젝트가 이 이벤트를 통해 얻어질 수 있으며, 추후 Mount 를 통해 추가되는 자식 오브젝트 또한 이 이벤트로 얻어질 수 있습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
local Quad = require(path.to.module).Init()
local Class = Quad.Class
local Mount = Quad.Mount
local Evnet = Quad.Event

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {
        self "_holder";
    }
end
function myClass:AfterRender()
    self.ChildAdded:Connect(function(child)
        print(child.Name)
    end)
end

local myImported = Class(myClass)
local myButton = myImported{
    -- Init 에 ChildAdded 를 넣은것이 아니기 때문에 출력되지 않습니다.
    -- 그러나 AfterRender 에 :GetChildren 을 넣어 이 오브젝트를
    -- 얻어올 수 있습니다.
    TextButton { Name = "Hello "};
}

Mount(myButton, TextButton{ Name = "Hello" })
Mount(myButton, TextButton{ Name = "World" })


:GetChildren()

자식 오브젝트들을 가져오는 함수입니다. :Init():AfterRender() 에서는 빈 테이블을 반환합니다. 자식의 순서는 일정하지 않습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
local Quad = require(path.to.module).Init()
local Class = Quad.Class
local Mount = Quad.Mount
local Evnet = Quad.Event

local TextButton = Class "TextButton"

local myClass = Class.Extend()
function myClass:Render(props)
    return TextButton {
        self "_holder";
    }
end
function myClass:AfterRender()
    for i,v in ipairs(self:GetChildren()) do
        print(v.Name)
    end
end

local myImported = Class(myClass)
local myButton = myImported{
    TextButton { Name = "Hello World"};
}

-- 나중에 추가하는 오브젝트는 출력되지 않습니다.
Mount(myButton, TextButton{ Name = "Hello" })
Mount(myButton, TextButton{ Name = "World" })