[사족 : 사실 제일 먼저 다루려고 했으나, 워낙 익숙한 툴이라 잊혀졌습니다. API에 관심이 있으신 분은 어떤 개발툴도(사실 더 아는 개발툴도 없습니다😆 ) 좋으니 말씀해주시면 최대한 작성해보겠습니다]
VBA는 Excel를 다루는 사용자에게 가장 유용하고 접근이 쉬운 개발툴로, 이를 이용하여 MIDAS Civil API를 사용하는 예제를 작성해봅니다. 이미 다수의 문서에서 API 명령어에 대한 내용을 수록하였으므로, 여기서는 VBA를 다루는 것에 집중하도록 하겠습니다.
1️⃣ Json 데이터 다루기
VBA에서 Json 데이터를 손쉽게 다루기 위해서는 Dictionary Class 를 이용하는게 경험상 좋습니다.
초창기 예제 작성시 String 데이터로 작성하였으나, 이는 Json Data가 커질수록 사용하기 어렵고 (Hell of Chr(34)), 특히 Get Method를 이용하여 불러온 데이터를 다루기가 어렵습니다. 하지만 다행스럽게 Dictionary Class를 Json 포맷으로 Converter 해주는 Code가 있으므로, 이를 사용하면 더욱 더 편하게 작성이 가능합니다.
Dictionary Class는, 메뉴->도구->참조-> Microsoft Scripting Runtime을 활성화하여 사용합니다.
아래와 같이 활성화하지 않고 사용할 수 있지만, 불편하므로 활성화하고 사용하시길 추천드립니다.
'활성화하지 않고 사용할 시 선언 방법(late binding)
Dim DicEx As Object 'Declare
Set DicEx = CreateObject("Scripting.Dictionary") 'Create
'활성화 후 선언하는 방법(early binding)
Dim DicEx As Dictionary 'Declare
Set DicEx = New Dictionary 'Create
Dictionary Class는 다음과 같은 설정값을 가집니다.
속성
Count : 현재 저장된 개체의 갯수를 반환합니다.
Item(“KeyValue”) : 지정한 키의 Value를 불러오거나, 대체할 수 있습니다.
Key(“KeyValue”) : 지정한 키의 Key를 불러오거나, 대체할 수 있습니다.
CompareMode : Dictionary 개체에서 문자열 키를 비교하는 모드를 설정하여 반환합니다.(Binary, text 모드가 있으나, 굳이 변경할 필요는 없습니다.)
메소드
Add : 키와 값을 추가합니다. 존재하는 키이면 오류가 발생합니다.
Remove : 지정한 키와 할당되어 있는 값을 제거합니다. 존재하지 않는 키이면 오류가 발생합니다.
RemoveAll : 모든 키와 값을 제거합니다.
Exists : 지정한 키가 존재하면 True를 반환하고, 아니면 False를 반환합니다.
keys : 모든 키들을 배열로 반환합니다.
Items : 모든 값들을 배열로 반환합니다.
Json 양식을 VBA에서는 공식 지원하는 것이 없으므로, 아래 링크에 있는 JsonConvertor를 이용합니다.
GitHub - VBA-tools/VBA-JSON: JSON conversion and parsing for VBA
2️⃣ Dictionary 사용 예제
Example 1
Option Explicit
Option Base 1
Private Sub DicExample()
Dim i As Integer
Dim varItm, varMItm, varMKey As Variant
Dim lngCnt As Long
Dim blnKey As Boolean
Dim dicExm As Dictionary
'Key와 Value를 할당합니다.
Set dicExm = New Dictionary
dicExm.Add 1, "Apple"
dicExm.Add 2, "Banana"
dicExm.Add "3", "Cherry"
'Dicrionay Class를 Json 포맷으로 변환하고 출력합니다.
Debug.Print JsonConverter.ConvertToJson(dicExm)
Debug.Print "=================================================="
'객체의 갯수를 반환합니다.
lngCnt = dicExm.Count
Debug.Print lngCnt
Debug.Print "=================================================="
'특정 key를 가지는 value를 반환합니다.
varItm = dicExm.Item(1)
Debug.Print varItm
Debug.Print "=================================================="
'키값을 모두 변경합니다.
dicExm.Key(1) = 3
dicExm.Key("3") = 4
Debug.Print JsonConverter.ConvertToJson(dicExm)
Debug.Print "=================================================="
'지정한 Key의 존재 유무를 파악합니다.
blnKey = dicExm.Exists(4)
Debug.Print blnKey
Debug.Print "=================================================="
'Key를 배열로 반환합니다.
varMKey = dicExm.Keys
For i = LBound(varMKey) To UBound(varMKey)
Debug.Print varMKey(i)
Next i
Debug.Print "=================================================="
'Value를 배열로 반환합니다.
varMItm = dicExm.Items
For i = LBound(varMItm) To UBound(varMItm)
Debug.Print varMItm(i)
Next i
Debug.Print "=================================================="
'모든 Key와 Value를 삭제합니다.
dicExm.RemoveAll
Debug.Print dicExm.Count
Debug.Print "=================================================="
End Sub
VBA는 DeepCopy를 실행하는 명령어가 따로 없습니다(제가 알기론). 따라서 아래 예제와 같이 DeepCopy 형태의 Function을 이용할 수 있습니다. 혹은 아래와 같이 만들고 버리는 반복 작업을 통해 변수의 개수를 최소화하여 작성할 수 있습니다.
'예를 들어
Set dic = new dictionary 'Create
'dic 상위 클래스에 등록
Set dic = nothing 'Release
반복
이에 대한 내용은 깊은 복사 VS 얕은 복사 (velog.io) 이걸 참조하시면 이해가 빠르실 겁니다.
Example 2
Private Sub DicExample2()
Dim i, j As Integer
Dim dicSub2, dicSub1, dicMain As Dictionary
'배열을 Value에 등록합니다.
Dim intItm(8) As Integer
Set dicMain = New Dictionary
For i = 1 To 8
intItm(i) = i
Next i
dicMain.Add "Integer", intItm
Debug.Print JsonConverter.ConvertToJson(dicMain)
Set dicMain = Nothing
'여러단계의 깊이를 가지는 Dicionary 구조 예제입니다
'GRUP를 예제로 다음 구조를 가지는 Dictionary를 만들어 봅니다
'"GRUP":{
' "1":{
' "NAME":"G1"
' "E_LIST":[1,2,3,4,5]
' },
' "2":{
' "NAME":"G2"
' "E_LIST":[6,7,8,9,10]
' }
' }
Dim intElmList(5) As Integer
Set dicSub1 = New Dictionary: Set dicMain = New Dictionary
For i = 1 To 2
For j = 1 To 5
intElmList(j) = 5 * (i - 1) + j
Next j
Set dicSub2 = New Dictionary 'Setting dicSub2 as new dictonary
dicSub2.Add "NAME", "G" & i
dicSub2.Add "E_LIST", intElmList
'dicSub2.Add "E_LIST", Array(1, 2, 3, 4, 5) '간단한 데이터는 Array 함수로 각각 처리할 수도 있습니다.
dicSub1.Add i, dicSub2
Set dicSub2 = Nothing 'Release dicsub2
Next i
dicMain.Add "GRUP", dicSub1
Debug.Print JsonConverter.ConvertToJson(dicMain)
Set dicSub1 = Nothing: Set dicMain = Nothing
'LCOM 과 같이 Dictionary 구조를 Array로 가지는 것에 대한 예제입니다.
'다음 구조를 예제로 한번 작성해 보겠습니다.
' {
' "LCOM": {
' "1": {
' "NAME": "DC",
' "KIND": "GEN",
' "ACTIVE": "ACTIVE",
' "bES": false,
' "iTYPE": 0,
' "DESC": "",
' "iSERV_TYPE": 0,
' "nLCOMTYPE": 0,
' "nSEISTYPE": 0,
' "vCOMB": [
' {
' "ANAL": "CS",
' "LCNAME": "Dead Load",
' "FACTOR": 1
' }
' ]
' },
' "2": {
' "NAME": "CR/SH",
' "KIND": "GEN",
' "ACTIVE": "ACTIVE",
' "bES": false,
' "iTYPE": 0,
' "DESC": "",
' "iSERV_TYPE": 0,
' "nLCOMTYPE": 0,
' "nSEISTYPE": 0,
' "vCOMB": [
' {
' "ANAL": "CS",
' "LCNAME": "Creep Secondary",
' "FACTOR": 1
' },
' {
' "ANAL": "CS",
' "LCNAME": "Shrinkage Secondary",
' "FACTOR": 1
' }
' ]
' }
' }
Dim dicComb(), dicCmn As Dictionary
Set dicCmn = New Dictionary
Set dicSub1 = New Dictionary: Set dicMain = New Dictionary
dicCmn.Add "NAME", ""
dicCmn.Add "KIND", "GEN"
dicCmn.Add "ACTIVE", "ACTIVE"
dicCmn.Add "bES", False
dicCmn.Add "iTYPE", 0
dicCmn.Add "DESC", ""
dicCmn.Add "iSERV_TYPE", 0
dicCmn.Add "nLCOMTYPE", 0
dicCmn.Add "nSEISTYPE", 0
dicCmn.Add "vCOMB", ""
dicSub1.Add 1, DeepCopy(dicCmn) 'for DeepCopy
dicSub1.Add 2, DeepCopy(dicCmn) 'for DeepCopy
ReDim dicComb(1)
Set dicComb(1) = New Dictionary
dicComb(1).Add "ANAL", "CS"
dicComb(1).Add "LCNAME", "Dead Load"
dicComb(1).Add "FACTOR", 1
dicSub1.Item(1)("NAME") = "DC"
dicSub1.Item(1)("vCOMB") = dicComb
ReDim dicComb(2)
For i = 1 To 2: Set dicComb(i) = New Dictionary: Next i
dicComb(1).Add "ANAL", "CS"
dicComb(1).Add "LCNAME", "Creep Secondary"
dicComb(1).Add "FACTOR", 1
dicComb(2).Add "ANAL", "CS"
dicComb(2).Add "LCNAME", "Shrinkage Secondary"
dicComb(2).Add "FACTOR", 1
dicSub1.Item(2)("NAME") = "CR/SH"
dicSub1.Item(2)("vCOMB") = dicComb
dicMain.Add "LCOM", dicSub1
Debug.Print JsonConverter.ConvertToJson(dicMain)
End Sub
Private Function DeepCopy(dic As Dictionary) As Dictionary
Dim Key As Variant
Set DeepCopy = Nothing
Set DeepCopy = New Dictionary
For Each Key In dic.Keys
DeepCopy.Add Key, dic(Key)
Next Key
End Function
3️⃣ JsonConvertor 예제
JsonConverter
Private Sub JsonConverterEX()
Dim Json As String
Dim dicJson As Dictionary
'Json 포맷의 String Data 작성
Json = "{" & Chr(34) & "NODE" & Chr(34) & ":{" & Chr(34) & "1001" & Chr(34) & ":{" _
& Chr(34) & "X" & Chr(34) & ":1.1," & Chr(34) & "Y" & Chr(34) & ":2.2," & Chr(34) & "Z" & Chr(34) & ":3.3}}}"
Debug.Print Json
'Json 포맷의 String Data를 Dictionary로 변환
Set dicJson = JsonConverter.ParseJson(Json)
Debug.Print dicJson.Item("NODE")("1001")("X")
Debug.Print dicJson.Item("NODE")("1001")("Y")
Debug.Print dicJson.Item("NODE")("1001")("Z")
dicJson.Item("NODE")("1001")("X") = 4.4
dicJson.Item("NODE")("1001")("Y") = 5.5
dicJson.Item("NODE")("1001")("Z") = 6.6
'Dictionay Data를 Json String 데이터로 변환
Dim dicTojson As String
dicTojson = JsonConverter.ConvertToJson(dicJson)
Debug.Print dicTojson
End Sub
4️⃣ Simple Beam 예제
VBA에서 Http Request는 “WinHttp.WinHttpRequest.5.1”를 사용합니다.
최대한 많은 API를 이용하여 단순보를 작성하는 예제를 만들었습니다. 첨부파일을 참고해주세요.
VBA Ex.mp4
5️⃣ 추가
(1) Runtime error - Timeout
아래 코드는 위에 예제에서 활용한 HttpRequest Function 입니다.
Function WebRequest(Method As String, Command As String, Body As String) As String
Dim TCRequestItem As Object
Dim URL As String
Set TCRequestItem = CreateObject("WinHttp.WinHttpRequest.5.1")
URL = "http://localhost:10024"
URL = URL & Command
TCRequestItem.Open Method, URL, False
TCRequestItem.SetRequestHeader "Content-type", "application/json"
TCRequestItem.Send Body
WebRequest = TCRequestItem.ResponseText
End Function
간혹, 제품으로부터 Response를 받기까지 시간이 지연되어, 아래와 같은 경고창과 함께 VBA 작동이 멈추는 경우가 있습니다. (절점과 요소수가 많을 경우 등등)
그럴 경우 아래와 같이 코드 한 줄을 삽입해주시면 됩니다. 단위는 Milliseconds 입니다.
Function WebRequest(Method As String, Command As String, Body As String) As String
Dim TCRequestItem As Object
Dim URL As String
Set TCRequestItem = CreateObject("WinHttp.WinHttpRequest.5.1")
'SetTimeouts(resolveTimeout, ConnectTimeout, SendTimeout, ReceiveTimeout)
TCRequestItem.SetTimeouts 60000, 60000, 60000, 60000
URL = "http://localhost:10024"
URL = URL & Command
TCRequestItem.Open Method, URL, False
TCRequestItem.SetRequestHeader "Content-type", "application/json"
TCRequestItem.Send Body
WebRequest = TCRequestItem.ResponseText
End Function
(2) float point error (부동소수점 에러)
이 항은 수치해석에 잘 알려진 에러인 부동소수점 에러를 VBA에서 어떻게 다루는지에 대해 설명해봅니다.
부동소수점의 에러는 컴퓨터가 받아들이는 숫자를 처리하는 방식에 의해서 생기는 걸로 이해하고 있습니다.
(관심이 있으신 분은 Floating-point arithmetic - Wikipedia 여길 참고해보세요.)
예를 들어 다음과 같은 코드를 실행시켜 보겠습니다.
Option Explicit
Sub Test()
Dim i As Integer
Dim sngVar As Single
Dim dblVar As Double
For i = 1 To 10000
sngVar = sngVar + 0.0001
Next i
Debug.Print TypeName(sngVar) & " : " & sngVar
For i = 1 To 10000
dblVar = dblVar + 0.0001
Next i
Debug.Print TypeName(dblVar) & " : " & dblVar
End Sub
0.0001를 만번 더했는데, 원했던 결과인 1이 나오질 않습니다. 게다가 자료형에 따라 그 오차도 차이가 있습니다. 이런 문제는 프로그램이면 모두 가지고 있는 문제입니다. 아래는 엑셀 예제입니다. 간단한 연산임에도 불구하고 9번째 행은 우리가 원하는 결과를 보여주지 못합니다.
이런 에러는 간단하게 유효 자릿수를 주는 방식으로 해결할 수도 있지만, 조금도 근본적인 해결방법이 있는 경우가 있습니다. 바로 자료형을 바꿔주는 겁니다.
VBA에서 이런 문제에 쓸 수 있는 자료형은 바로 Decimal 자료형입니다.
아래와 같이 0.0001를 Decimal 자료형으로 바꿔주어 더해주거나, Decimal 자료형으로 선언해서 쓰는 방법 등이 있습니다.(Decimal 은 Variant로 선언합니다.)
Option Explicit
Sub Test()
Dim i As Integer
Dim sngVar As Single
Dim dblVar As Double
Dim decVar1 As Variant
Dim decVar2 As Variant
For i = 1 To 10000
sngVar = sngVar + CDec(0.0001)
Next i
Debug.Print TypeName(sngVar) & " : " & sngVar
For i = 1 To 10000
dblVar = dblVar + CDec(0.0001)
Next i
Debug.Print TypeName(dblVar) & " : " & dblVar
For i = 1 To 10000
decVar1 = decVar1 + CDec(0.0001)
Next i
Debug.Print TypeName(decVar1) & " : " & dblVar
For i = 1 To 10000
decVar2 = decVar2 + 0.0001
Next i
Debug.Print TypeName(decVar2) & " : " & dblVar
End Sub