네이버 국어사전/영어사전 검색 도구 동작 방식과 소스코드
네이버 국어사전/영어사전 검색 도구 동작 방식과 소스코드 에 대해 설명한다.
이전 글에서 이어지는 내용이다.
1. 네이버 국어사전/영어사전 검색 도구 동작 방식과 주의사항
사용자가 웹브라우저를 이용하여 검색어를 네이버 서비스에 검색을 요청(Request)하고, 네이버 서버는 요청에 대한 처리결과를 응답(Response)한다.
(Web 동작 방식에 대한 자세한 내용은 아래 구글 검색결과의 글을 읽어보기 바란다.)
https://www.google.co.kr/search?q=web+동작+방식
네이버 사전 서비스의 검색 요청(Request)과 응답(Response)에 대해 자세히 살펴보자.
1.1. 네이버 사전 검색 요청과 응답
웹 브라우저를 통해 서버와 주고 받는 내용을 확인하는 방법은 여러 가지가 있는데, 여기에서는 Fiddler Web Debugger로 설명하겠다.
아래는 네이버 국어사전에서 “가입” 이라는 단어를 검색했을 때 요청과 응답내용을 Fiddler에서 확인한 결과이다.
- URL, Content-Type: 아래 내용을 확인할 수 있다.
- Protocol: HTTPS
- Host: ko.dict.naver.com
- URL: /api3/koko/search?query=%EA%B0%80%EC%9E%85&m=pc&hid=162470754628591300
- 여기에서 “%EA%B0%80%EC%9E%85″는 “가입”이 URL Encoding된 문자열이다.
- 요청(Request) Header
- User-Agent, Cookie 등의 내용을 확인할 수 있다.
- 응답(Response) 내용
- Content-Type: application/json;charset=UTF-8
- Response 내용이 json 형식이고, character-set은 UTF-8로 encoding되어 있음을 알 수 있다.
- Content-Length: 50814
- Response 내용이 50,814 Byte, 약 50KB 임을 알 수 있다.
- Content-Body: {“searchResultMap”:{“searchResultListMap”:{“WORD”:{“query”:”가입”, …
- JSON string이고, “JSON” 탭에서 확인하면 다음과 같이 계층적 구조를 가지고 있다.
- Content-Type: application/json;charset=UTF-8
1.2. 응답결과 형식 변경 (HTML –> JSON)
이 도구는 네이버 Open API를 사용하지 않고 Web Request, Response 방식을 이용한다.
정확하지는 않으나, 2018년 12월을 전후로 응답결과의 형식이 변경되었다. 그 이전에는 HTML 형식이었는데, 이 시기에 우연히 Fiddler로 확인해 보니 JSON 형식으로 응답이 변경되었음을 알게 되었다.
이 도구의 첫 버전은 응답이 HTML 형식일 때 만들어졌다. HTML에서 필요한 항목을 추출하였는데, 네이버가 HTML의 구조를 변경할 때마다 제대로 동작하지 않아 변경된 HTML구조에 맞춰서 매번 소스코드를 변경해 줘야 했었다. 응답결과 형식이 JSON으로 변경된 이후로는 소스코드 변경없이 잘 동작하고 있다.
1.3. 사용상 주의사항
네이버가 사전 검색결과를 JSON 형식으로 제공한다고 공식적으로 알렸는지 확인할 수 없다. JSON의 구조에 대한 문서도 공개되어 있지 않은 것으로 보인다.
(만약, 공개된 뉴스나 자료가 있다면 댓글로 알려주기 바란다.)
이런 이유로 어느 날 갑자기 동작하지 않을 수 있으니 주의하기 바란다.
2. 구현
2.1. 전체 흐름 요약
검색할 단어를 URL Encoding하고 GetDataFromURL 함수를 실행하여 가져온 JSON 검색결과를 parsing하여 필요한 항목을 추출한다.
Dim aWord As String, sBaseURL As String, sWord As String aWord = "가입" sBaseURL = "https://ko.dict.naver.com/api3/koko/search?query=%s" '기본 URL sWord = URLEncodeUTF8(aWord) '검색어 URL Encoding Dim sURL As String, sURLData As String, oParsedDic As Dictionary sURL = Replace(sBaseURL, "%s", sWord) '기본 URL에 검색어 대입 sURLData = GetDataFromURL(sURL, "GET", "", "utf-8") 'URL에서 결과 가져오기 Set oParsedDic = JsonConverter.ParseJson(sURLData) 'JSON결과를 Dictionary로 변환 'JSON이 변환된 Dictionary에서 검색결과에 해당하는 항목 추출 '시작 Path: oParsedDic("searchResultMap")("searchResultListMap")("WORD")("items")
주요 함수에 대해 살펴보자.
2.2. URL Encoding하기 (URLEncodeUTF8 소스코드)
검색 요청할 URL을 URLEncoding한 문자열로 반환한다. ADODB.Stream 클래스를 사용하였다.
Public Function URLEncodeUTF8( _ StringVal As String, _ Optional SpaceAsPlus As Boolean = False _ ) As String Dim bytes() As Byte, b As Byte, i As Integer, space As String If SpaceAsPlus Then space = "+" Else space = "%20" If Len(StringVal) > 0 Then With New ADODB.Stream .Mode = adModeReadWrite .Type = adTypeText .CharSet = "UTF-8" .Open .WriteText StringVal .Position = 0 .Type = adTypeBinary .Position = 3 ' skip BOM bytes = .Read End With ReDim Result(UBound(bytes)) As String For i = UBound(bytes) To 0 Step -1 b = bytes(i) Select Case b Case 97 To 122, 65 To 90, 48 To 57, 45, 46, 95, 126 Result(i) = Chr(b) Case 32 Result(i) = space Case 0 To 15 Result(i) = "%0" & Hex(b) Case Else Result(i) = "%" & Hex(b) End Select Next i URLEncodeUTF8 = Join(Result, "") End If End Function
ADODB 라이브러리를 사용하기 위해서 “Microsoft ActiveX Data Object 6.1 Library”를 참조 추가해야 한다. 엑셀 화면에서 Alt + F11 키를 누르고 VBA Editor로 전환하여 추가해 주면 된다.
2.3. Request & Get Response (GetDataFromURL 함수 소스코드)
“WinHttp.WinHttpRequest” 클래스를 이용하여 Request header, option 정보를 설정하고, 검색 URL을 방문하여 결과를 얻어온다. CreateObject로 개체를 생성하는 late binding 방식이라 라이브러리 참조 추가할 필요는 없다.
Function GetDataFromURL(strURL, strMethod, strPostData, Optional strCharSet = "UTF-8") Dim lngTimeout Dim strUserAgentString Dim intSslErrorIgnoreFlags Dim blnEnableRedirects Dim blnEnableHttpsToHttpRedirects Dim strHostOverride Dim strLogin Dim strPassword Dim strResponseText Dim objWinHttp lngTimeout = 59000 strUserAgentString = "http_requester/0.1" intSslErrorIgnoreFlags = 13056 ' 13056: ignore all err, 0: accept no err blnEnableRedirects = True blnEnableHttpsToHttpRedirects = True strHostOverride = "" strLogin = "" strPassword = "" Set objWinHttp = CreateObject("WinHttp.WinHttpRequest.5.1") '-------------------------------------------------------------------- 'objWinHttp.SetProxy 2, "xxx.xxx.xxx.xxx:xxxx", "" 'Proxy를 사용하는 환경에서 설정 '-------------------------------------------------------------------- objWinHttp.SetTimeouts lngTimeout, lngTimeout, lngTimeout, lngTimeout objWinHttp.Open strMethod, strURL If strMethod = "POST" Then objWinHttp.SetRequestHeader "Content-type", "application/x-www-form-urlencoded; charset=UTF-8" Else objWinHttp.SetRequestHeader "Content-type", "text/html; charset=euc-kr" End If If strHostOverride <> "" Then objWinHttp.SetRequestHeader "Host", strHostOverride End If objWinHttp.Option(0) = strUserAgentString objWinHttp.Option(4) = intSslErrorIgnoreFlags objWinHttp.Option(6) = blnEnableRedirects objWinHttp.Option(12) = blnEnableHttpsToHttpRedirects If (strLogin <> "") And (strPassword <> "") Then objWinHttp.SetCredentials strLogin, strPassword, 0 End If On Error Resume Next objWinHttp.Send (strPostData) objWinHttp.WaitForResponse If Err.Number = 0 Then If objWinHttp.Status = "200" Then 'GetDataFromURL = objWinHttp.ResponseText GetDataFromURL = BinaryToText(objWinHttp.ResponseBody, strCharSet) Else GetDataFromURL = "HTTP " & objWinHttp.Status & " " & _ objWinHttp.StatusText End If Else GetDataFromURL = "Error " & Err.Number & " " & Err.Source & " " & _ Err.Description End If On Error GoTo 0 Set objWinHttp = Nothing End Function
2.4. Response(검색결과) JSON 문자열
Response(검색결과) JSON 문자열은 상당히 많은 정보를 포함하고 있다. 들여쓰기와 행구분이 없어 보기가 어려운데, 보기 좋게 정리하면 아래와 같다. (일부만 발췌함)
{ "searchResultMap": { "searchResultListMap": { "WORD": { "query": "가입", "queryRevert": "", "items": [ { "rank": "1", "gdid": "8800000f_4002c436c93d4bb38d3e58632fe00af0", "matchType": "exact:entry", "entryId": "4002c436c93d4bb38d3e58632fe00af0", "serviceCode": "1", "languageCode": "KOKO", "expDictTypeForm": "단어", "dictTypeForm": "2", "sourceDictnameKO": "표준국어대사전", "sourceDictnameOri": "Standard Korean Dict.", "sourceDictnameLink": "https://stdict.korean.go.kr/main/main.do", ... "expEntry": "<strong>가입</strong>", ... "destinationLink": "#/entry/koko/4002c436c93d4bb38d3e58632fe00af0", ... "meansCollector": [ { "partOfSpeech": "명사", "partOfSpeech2": "noun", "means": [ { "order": "1", "value": "조직이나 단체 따위에 들어가거나, 서비스를 제공하는 상품 따위를 신청함.", ... "exampleOri": "<strong>가입</strong> 신청서.", ... }, { "order": "2", "value": "새로 더 집어넣음.", ... "exampleOri": "원고 중간에 수정된 내용의 <strong>가입</strong>이 발견되었다.", ... }, { "order": "3", "value": "조약문의 인증 절차 없이, 그 조약에 드는 행위. 의사 표시만으로 당사자가 될 수 있게 하여 법 공동체...", ... "languageGroup": "법률", ... "exampleTrans": null, ... } ] } ], "similarWordList": [], "antonymWordList": [ { "antonymWordName": "탈퇴", "antonymWordLink": "#/entry/koko/14e89175152b46569c2a2b6360e835ad" } ], "expAliasEntryAlwaysList": [], "expAliasGeneralAlwaysList": [ { "originLanguageValue": "加入" } ], ... }, { "rank": "2", "gdid": "881857e6_e12c4e3432cf458c929bd49c929fd80b", "matchType": "exact:entry", "entryId": "e12c4e3432cf458c929bd49c929fd80b", "serviceCode": "1", "languageCode": "KOKO", "expDictTypeForm": "단어", "dictTypeForm": "2", "sourceDictnameKO": "우리말샘", "sourceDictnameOri": "Urimalsaem", "sourceDictnameLink": "https://opendict.korean.go.kr/main", ... "expEntry": "<strong>가입</strong>", ... "destinationLink": "#/entry/koko/e12c4e3432cf458c929bd49c929fd80b", ... "meansCollector": [ { "partOfSpeech": "명사", "partOfSpeech2": "noun", "means": [ { "order": "", "value": "어떤 개체군에 새로운 개체가 더해지는 것. 단, 일정한 발육 단계에 도달한 개체만 해당된다.", ... } ] } ], "similarWordList": [], "antonymWordList": [], ... }, ], "total": 96, "sectionType": "WORD", "revert": "", "orKEquery": null } } } }
2.5. JSON parser
문자열 함수(MID, INSTR 등)를 사용하여 JSON 문자열에서 원하는 항목을 추출해 낼 수는 있으나, 탐색이 복잡하고 코드가 매우 지저분해 진다.
Python으로 구현하면 간단히 json module을 import하고, json 클래스를 사용하면 된다. VBA는 공개된 라이브러리가 많지 않은데, 다행히 github에 공개된 JSON parser가 있어서 잘 사용하였다.
https://github.com/VBA-tools/VBA-JSON
이 JSON parser의 소스코드는 1,123 행이나 되어 블로그에 올려놓지는 않는다. 필요한 분들은 위 URL에서 소스코드를 확인하기 바란다. JSON parser를 사용하는 간단한 예시는 다음과 같다.(위 github에 공개된 코드)
Dim Json As Object Set Json = JsonConverter.ParseJson("{""a"":123,""b"":[1,2,3,4],""c"":{""d"":456}}") ' Json("a") -> 123 ' Json("b")(2) -> 2 ' Json("c")("d") -> 456 Json("c")("e") = 789 Debug.Print JsonConverter.ConvertToJson(Json) ' -> "{"a":123,"b":[1,2,3,4],"c":{"d":456,"e":789}}" Debug.Print JsonConverter.ConvertToJson(Json, Whitespace:=2) ' -> "{ ' "a": 123, ' "b": [ ' 1, ' 2, ' 3, ' 4 ' ], ' "c": { ' "d": 456, ' "e": 789 ' } ' }"
2.6. 검색 버튼 클릭 이벤트 소스코드
“사전검색” 시트에서 “네이버 사전 검색” 버튼을 클릭했을 때 실행되는 코드이다. 다음 내용이 구현되어 있다.
- 옵션 설정이 잘 되어 있는지 확인한다.
- 검색어에 대해 사전 검색을 반복 실행하여 결과를 시트에 표시한다.
- 표시되는 결과는 matchType, searchEntry, meaning, link, 유의어, 반의어이다.
- 실행중 “검색 중지” 버튼이 눌렸다면 반복을 멈춘다.
Private Sub cmdRunDicSearch_Click() Range("A1").Select DoEvents Dim bIsKorDicSearch As Boolean, bIsEngDicSearch As Boolean, sTargetDic As String bIsKorDicSearch = chkKorDic.Value: bIsEngDicSearch = chkEngDic.Value If (Not bIsKorDicSearch) And (Not bIsEngDicSearch) Then MsgBox "검색 대상 사전중 적어도 1개는 선택해야 합니다", vbExclamation + vbOKOnly, "검색 대상 사전 확인" Exit Sub End If Dim bIsMatchTypeExact As Boolean, bIsMatchTypeTermOr As Boolean, bIsMatchTypeAllTerm As Boolean '검색결과 표시 설정 bIsMatchTypeExact = chkMatchTypeExact.Value: bIsMatchTypeTermOr = chkMatchTypeTermOr.Value: bIsMatchTypeAllTerm = chkMatchTypeAllTerm.Value If (bIsMatchTypeExact Or bIsMatchTypeTermOr Or bIsMatchTypeAllTerm) = False Then MsgBox "검색결과 표시 설정중 적어도 하나는 선택해야 합니다.", vbExclamation + vbOKOnly, "확인" Exit Sub End If If bIsKorDicSearch And Not bIsEngDicSearch Then sTargetDic = "국어사전" If Not bIsKorDicSearch And bIsEngDicSearch Then sTargetDic = "영어사전" If bIsKorDicSearch And bIsEngDicSearch Then sTargetDic = "국어사전, 영어사전" Dim lMaxResultCount As Long lMaxResultCount = CInt(txtMaxResultCount.Value) If MsgBox("사전 검색을 시작하시겠습니까?" + vbLf + _ "대상 사전: " + sTargetDic + vbLf + _ "결과출력 제한개수: " + CStr(lMaxResultCount) _ , vbQuestion + vbYesNoCancel, "확인") <> vbYes Then Exit Sub Dim i As Long, iResultOffset As Long bIsWantToStop = False DoEvents Dim sWord As String, oKorDicSearchResult As TDicSearchResult, oEngDicSearchResult As TDicSearchResult Dim oBaseRange As Range Set oBaseRange = Range("검색결과Header").Offset(1, 0) oBaseRange.Select For i = 0 To 100000 If bIsWantToStop Then MsgBox "사용자의 요청으로 검색을 중단합니다.", vbInformation + vbOKOnly, "확인" Exit For End If If chkSkipIfResultExists.Value = True And _ oBaseRange.Offset(i, 1) <> "" Then GoTo Continue_For '이미 내용이 있으면 Skip sWord = oBaseRange.Offset(i) If sWord = "" Then Exit For oBaseRange.Offset(i).Select Application.ScreenUpdating = False If bIsKorDicSearch Then '국어사전 검색결과 표시 oKorDicSearchResult = DoDicSearch(dtsKorean, sWord, bIsMatchTypeExact, bIsMatchTypeTermOr, bIsMatchTypeAllTerm, lMaxResultCount) oBaseRange.Offset(i, 1).Select With oKorDicSearchResult oBaseRange.Offset(i, 1) = .sMatchType oBaseRange.Offset(i, 2) = .sSearchEntry oBaseRange.Offset(i, 3) = .sMeaning If oKorDicSearchResult.sLinkURL <> "" Then With ActiveSheet.Hyperlinks.Add(Anchor:=oBaseRange.Offset(i, 4), Address:=.sLinkURL, TextToDisplay:="네이버국어사전 열기: " & .sLinkWord) .Range.Font.Size = 8 End With End If oBaseRange.Offset(i, 5) = .sSynonymList oBaseRange.Offset(i, 6) = .sAntonymList End With End If If bIsEngDicSearch Then '영어사전 검색결과 표시 oEngDicSearchResult = DoDicSearch(dtsEnglish, sWord, bIsMatchTypeExact, bIsMatchTypeTermOr, bIsMatchTypeAllTerm, lMaxResultCount) 'oBaseRange.Offset(i, 7).Select With oEngDicSearchResult oBaseRange.Offset(i, 7) = .sMatchType oBaseRange.Offset(i, 8) = .sSearchEntry oBaseRange.Offset(i, 9) = .sMeaning If oKorDicSearchResult.sLinkURL <> "" Then With ActiveSheet.Hyperlinks.Add(Anchor:=oBaseRange.Offset(i, 10), Address:=.sLinkURL, TextToDisplay:="네이버영어사전 열기: " & .sLinkWord) .Range.Font.Size = 8 End With End If oBaseRange.Offset(i, 11) = .sSynonymList oBaseRange.Offset(i, 12) = .sAntonymList End With End If Application.ScreenUpdating = True Continue_For: DoEvents Next i MsgBox "사전 검색을 완료하였습니다", vbOKOnly + vbInformation End Sub
2.7. 사전 검색 (DoDicSearch 소스코드)
검색어 한개에 대해 검색요청을 보내고 결과를 받은 다음 필요한 항목을 추출하여 반환하는 함수이다.
- JSON 문자열을 Dictionary로 parsing: 49행
- matchType, searchEntry, meaning, link, 유의어, 반의어 항목 추출: 53 ~ 106행
Const DICT_ROOT_URL_KO As String = "https://ko.dict.naver.com/" Const DICT_BASE_URL_KO As String = "https://ko.dict.naver.com/api3/koko/search?query=%s" Const DICT_ROOT_URL_EN As String = "https://en.dict.naver.com/" Const DICT_BASE_URL_EN As String = "https://en.dict.naver.com/api3/enko/search?query=%s" Public Enum DicToSearch dtsKorean = 1 dtsEnglish = 2 dtsAll = 10 End Enum Public Type TDicSearchResult sWord As String sMatchType As String sSearchEntry As String sMeaning As String sLinkURL As String sLinkWord As String sSynonymList As String sAntonymList As String End Type Public Function DoDicSearch(aDicToSearch As DicToSearch, aWord As String, _ bIsMatchTypeExact As Boolean, bIsMatchTypeTermOr As Boolean, bIsMatchTypeAllTerm As Boolean, _ aMaxResultCount As Long) As TDicSearchResult Dim sDicRootURL As String, sBaseURL As String, sURL As String, sURLData As String, sWord As String, oDicSearchResult As TDicSearchResult Dim oParsedDic As Dictionary Dim oItem As Dictionary, oMeansCollector As Dictionary, oMeans As Dictionary Dim oSimWords As Dictionary, oAntWord As Dictionary Dim sPOS As String, sMeaning As String, sLinkURL As String, sLinkWord As String Dim s유의어 As String, s유의어목록 As String, s반의어 As String, s반의어목록 As String Dim sMatchType As String, sSearchEntry As String, sHandleEntry As String Select Case aDicToSearch Case dtsKorean sDicRootURL = DICT_ROOT_URL_KO sBaseURL = DICT_BASE_URL_KO Case dtsEnglish sDicRootURL = DICT_ROOT_URL_EN sBaseURL = DICT_BASE_URL_EN End Select If aWord = "" Then Exit Function sWord = URLEncodeUTF8(aWord) sURL = Replace(sBaseURL, "%s", sWord) sURLData = GetDataFromURL(sURL, "GET", "", "utf-8") 'URL에서 결과 가져오기 Set oParsedDic = JsonConverter.ParseJson(sURLData) 'JSON결과를 Dictionary로 변환 Dim lMatchIdx As Long: lMatchIdx = 0 Dim lResultCount As Long: lResultCount = 0 For Each oItem In oParsedDic("searchResultMap")("searchResultListMap")("WORD")("items") lResultCount = lResultCount + 1 If (aMaxResultCount <> 0) And (lResultCount > aMaxResultCount) Then Exit For '결과출력 제한개수 초과시 Loop 종료 s유의어 = "": s반의어 = "" lMatchIdx = lMatchIdx + 1 'If oItem("matchType") <> "exact:entry" Then Exit For sHandleEntry = oItem("handleEntry") Select Case oItem("matchType") Case "exact:entry" sLinkWord = sHandleEntry sLinkURL = sDicRootURL + oItem("destinationLink") If Not bIsMatchTypeExact Then GoTo Continue_InnerFor Case "term:or" If Not bIsMatchTypeTermOr Then GoTo Continue_InnerFor Case "allterm:proximity:1.000000" If Not bIsMatchTypeAllTerm Then GoTo Continue_InnerFor Case Else End Select sMatchType = sMatchType + IIf(sMatchType = "", "", vbLf) & CStr(lMatchIdx) & ". " & oItem("matchType") sSearchEntry = sSearchEntry + IIf(sSearchEntry = "", "", vbLf) & CStr(lMatchIdx) & ". " & sHandleEntry For Each oMeansCollector In oItem("meansCollector") 'Debug.Print "품사: " & oMeansCollector("partOfSpeech") sPOS = "" If oMeansCollector.Exists("partOfSpeech") Then If Not IsNull(oMeansCollector("partOfSpeech")) Then sPOS = oMeansCollector("partOfSpeech") End If For Each oMeans In oMeansCollector("means") 'Debug.Print "뜻: " & oMeans("value") If oMeans.Exists("value") Then If Not IsNull(oMeans("value")) Then _ sMeaning = sMeaning + IIf(sMeaning = "", "", vbLf) & CStr(lMatchIdx) & ". " & IIf(sPOS = "", "", "[" & sPOS & "] ") & RemoveHTML(oMeans("value")) End If Next oMeans Next oMeansCollector For Each oSimWords In oItem("similarWordList") If oSimWords.Exists("similarWordName") Then _ s유의어 = s유의어 + IIf(s유의어 = "", "", ", ") & RemoveHTML(oSimWords("similarWordName")) Next oSimWords If s유의어 <> "" Then _ s유의어목록 = s유의어목록 & IIf(s유의어목록 = "", "", vbLf) & CStr(lMatchIdx) & ". " & sHandleEntry & ": " & s유의어 For Each oAntWord In oItem("antonymWordList") If oAntWord.Exists("antonymWordName") Then _ s반의어 = s반의어 + IIf(s반의어 = "", "", ", ") & RemoveHTML(oAntWord("antonymWordName")) Next oAntWord If s반의어 <> "" Then _ s반의어목록 = s반의어목록 & IIf(s반의어목록 = "", "", vbLf) & CStr(lMatchIdx) & ". " & sHandleEntry & ": " & s반의어 Continue_InnerFor: Next oItem If sMeaning = "" Then sMeaning = "#NOT FOUND#": sMatchType = sMeaning: sSearchEntry = sMeaning End If '결과값 반환 With oDicSearchResult .sWord = aWord .sMatchType = sMatchType .sSearchEntry = sSearchEntry .sMeaning = sMeaning .sLinkWord = sLinkWord .sLinkURL = Replace(sLinkURL, "#", "%23") 'Excel에서 #기호를 내부적으로 #20 - #20 으로 치환하는 것을 방지 .sSynonymList = s유의어목록 .sAntonymList = s반의어목록 End With DoDicSearch = oDicSearchResult End Function
이상으로 이 도구의 동작방식, 주의사항, 소스코드에 대해 살펴보았다. 도구를 사용해 보신 분들의 후기나, 궁금한 점, 필요한 기능 등 의견은 댓글로 남겨주기 바란다.
<< 관련 글 목록 >>
Absolutely with you it agree. It is excellent idea. I support you.
_ _ _ _ _ _ _ _ _ _ _ _ _ _
Nekultsy Ivan dxvk github
Thanks a lot.
Please take a look other article in my blog ^^