애들센스


나만의 이벤트 정의하기 오피스/VBA/Office.JS

프로그래밍을 하다보면 개체가 제공해주는 이벤트가 불만족스런 경우가 생긴다. 혹시 나만의 이벤트를 만들 수 있지 않을 까… 물론 가능하다. 다음은 Userform의 콤보상자를 바탕으로 여기에 내 이벤트를 만드는 예이다. 콤보상자에 데이터를 입력하면 새로 입려된 데이터는 콤보상자의 리스트로 들어간다. 그런데 이미 입력한 데이터라면 입력을 취소하는 예이다.

[DataComboBox.cls]
Option Explicit

'// define DataComboBox's an event, 'ItemAdded'
Public Event ItemAdded(strValue As String, blnCancel As Boolean)

'// p_cboCombo is a variable that refers to Userform's combobox
Private p_cboCombo As ComboBox

Public Property Set ComboBox(cboCombo As ComboBox)
'// reference to Userform's combobox
If p_cboCombo Is Nothing Then
Set p_cboCombo = cboCombo
End If
End Property

Public Property Get ComboBox() As ComboBox
'// return combox
Set ComboBox = p_cboCombo
End Property

Public Function AddDataItem(strValue As String)
'// blnCancel is a variable that determine
'// whether strValue can be added

Dim blnCancel As Boolean

blnCancel = False

'// Trigger User-defined Event, ItemAdded
RaiseEvent ItemAdded(strValue, blnCancel)

'// if blnCancel is True, strValue not added
If blnCancel = False Then
Me.ComboBox.AddItem strValue
End If
End Function
다음은 Userform코드이다
[frmDataCombo.frm]
Option Explicit

'// declare DataComboBox with its event
Private WithEvents mdcbCombo As DataComboBox

Private Sub cmdAdd_Click()
Dim strValue As String

'// save data of combobox to strValue
strValue = Me.cboData.Text

'// call DataComboBox's AddDataItem method
mdcbCombo.AddDataItem strValue
End Sub

Private Sub UserForm_Initialize()
'// when this form begins, make DataComboBox into instance
Set mdcbCombo = New DataComboBox

'// connect object to frmDataCombo's combobox
Set mdcbCombo.ComboBox = Me.cboData
End Sub

Private Sub mdcbCombo_ItemAdded(strValue As String, blnCancel As Boolean)
Dim lngCount As Long
Dim lngLoop As Long
Dim strItem As String
Dim varItem As Variant

'// count of list
lngCount = mdcbCombo.ComboBox.ListCount

'// while the loop, detemine if new Item already added.
'// if blnCancel is True, new item already exists
For lngLoop = 1 To lngCount
strItem = mdcbCombo.ComboBox.List(lngLoop - 1)
If strItem = strValue Then
blnCancel = True
Exit For
End If
Next
End Sub

사용자를 배려한 한/영 입력 오피스/VBA/Office.JS

옛날 얘기인데, 각종 업무용 프로그램을 액세스로 개발하여 밥먹고 사는 사람이 있었는 데, 개발해놓은 걸 보니 날짜입력을 그냥 텍스트박스로 처리해두고 있었다. Calendar컨트롤을 사용하면 입력도 편리하고 제대로 일자 포맷에 맞게 입력되었는 지 체크하는 수고도 덜 수 있을 텐데 그리 하지 않았다.

이유를 물어보니 , 본인도 그런 점을 알지만 다년 간의 경험상 그런 컨트롤을 사용하다보면 프로그램도 무거워지고(액세스는 디비를 같이 담고 있으니 더욱 그럴 만하다) 결정적으로 불안정해져 실행중 프로그램이 죽는 경우가 있다고 한다. 잔뜩 입력하다가 프로그램이 죽는 것보단 불편한게 나을 것이다. 게다가 달력컨트롤이 기본이 아니어서 PC마다 없는 경우가 생길 수 있다. '가지 많은 나무에 바람 잘 날 없다'는 속담이 통하는 대목이다.

입력중 불편한 사항중 하나는 화면을 안보고 입력하다보니, IME가 한글이 아니라 영문상태 또는 그 반대의 경우이다. 한글입력이 필요하거나 영문입력이 필요한 시점에서 IME가 자동으로 입력모드를 변경해주는 배려를 해줄 수 있다. 다음은 API를 사용하여 입력모드를 바꾸는 예이다.
Private Declare Function ImmGetContext _
Lib "imm32.dll" (ByVal hwnd As Long) As Long
Private Declare Function ImmSetConversionStatus _
Lib "imm32.dll" (ByVal himc As Long, _
ByVal dw1 As Long, ByVal dw2 As Long) As Long
Private Declare Function FindWindow Lib "user32" _
Alias "FindWindowA" (ByVal lpClassName As String, _
ByVal lpWindowName As String) As Long

Private Const IME_CMODE_NATIVE = &H1
Private Const IME_CMODE_HANGEUL = IME_CMODE_NATIVE
Private Const IME_CMODE_ALPHANUMERIC = &H0
Private Const IME_SMODE_NONE = &H0

Private Function GetAppHandle() As Long
Dim dVersion As Double

dVersion = Application.Version

If dVersion < 10 Then
GetAppHandle = FindWindow("XLMAIN", _
Application.Caption)
Else
GetAppHandle = Application.hwnd
End If
End Function

Private Sub demoChangeIME()
Dim hWndIME As Long
Dim hWndApp As Long

'// 엑셀의 핸들을 구함
hWndApp = GetAppHandle

'// 입력모드의 핸들을 구한다
hWndIME = ImmGetContext(hWndApp)

'// 영어 모드로 변환
Call ImmSetConversionStatus(hWndIME, _
IME_CMODE_ALPHANUMERIC, IME_SMODE_NONE)
InputBox "English 입력모드"

'// 한글 모드로 변환
Call ImmSetConversionStatus(hWndIME, _
IME_CMODE_HANGEUL, IME_SMODE_NONE)
InputBox "한국어 입력모드"

End Sub

Collection의 Key는 어디서 찾아야 하나? 오피스/VBA/Office.JS

이런 경우 VBA는 한심한 언어이다. 무슨 얘기인가 하면 Collection개체의 Add메서드를 사용할 때 딕셔너리처럼 {Key, Item}와 같이 Key를 같이 입력할 수 있다. 물론 Key는 유일해야 하므로 중복된 Key값은 받아주질 않는다.

그런데 Key를 입력하고 나면 Key값을 알 수 있는 방법이 없다. 도대체 Key를 만들어 입력해서 Key를 갖고 Item을 찾을 수 있게 해주면 뭐하나? Key을 돌려주는 프로퍼티나 메서드가 없는데…

이런 문제점에 대한 대안으로 Item에 Key를 포함해서 넣어주는 방법으로 해결한다. 즉 프로그래밍언어 차원이 아닌 개발방법으로서 몸으로 때우는 것이다.
Dim col As New Collection

col.Add Array("key#1", "first string"), "key#1"
col.Add Array("key#2", "second string"), "key#2"
col.Add Array("key#3", "third string"), "key#3"

'// print item's value in the collection
Dim item As Variant

For Each item In col
Debug.Print item(1)
Next

'// print item's key in the collection
Dim key As Variant

For Each key In col
Debug.Print key(0)
Next
위의 코드에서 Key#1, Key#2, Key#3는 종전처럼 입력하지만 이와 동시에 아이템 선두에 키값을 한번 더 입력하고 아이템 조회시 0번 인덱스를 Key로 삼아 사용하려는 것이다.

이런 VBA를 계속 쓰는 오피스개발자로서 눈물이 앞을 가린다.

[관련 포스팅]: VBA Collection

VBA로 만드는 알고리즘-리스트 오피스/VBA/Office.JS

데이터를 다룰 때 가장 많이 사용하는 것이 배열과 리스트인데, 오늘은 리스트 관련한 얘기를 하려고 한다. 리스트는 가장 인기있는 데이터구조인데, 배열과 달리 미리 크기가 정해져 있지 않고, 필요에 따라 데이터를 추가/삭제가 가능한 기능을 가지고 있다. VBA는 컬렉션(Collecton)개체라는 리스트를 제공하는 데(제공하는 것만도 감지덕지 하다), 그외 외부참조를 통해 딕셔너리나 닷넷의 ArrayList들이 사용가능하다.

프로그래밍하면서 가장 편리하다고 생각하는 것이 컬렉션개체인데, 역설적으로 기능이 정말 단촐해서 맘에 든다. 기능은 많은 데, 자주 사용하지 않는 것보단 단순하게 배우기 쉽고 활용도 좋다. 그렇지만 사람 욕심이라는 게, 내가 쓸 연장은 내가 만들고 싶은 욕구라는 게 있다.

다음은 별 기능은 없는 리스트 클래스이다. 내부구조를 보면 어차피 배열인데, 다만 배열보다 사용하기 편리한 메서드를 가지고 있다. 사실 언제 만들었는 지 모르겠다. 내가 만든 게 맞나 싶기도...(나의 기억은 조작되었을 지도...)
[CList.cls]
Option Explicit
Option Base 1

'// 사용자 정의 에러
'// 리스트의 상하한을 넘어 액세스하려는 경우 에러
Const ERR_LIST_BEYOND_BOUNDS As Long = 999

'// 리스트가 초기화되지 않은 상태에서 리스트를 사용하려는 경우 에러
Const ERR_LIST_NOT_INITIALIZED As Long = 998

Private Item_Data() As Variant
Private Item_Pointer As Long
Private Item_Init As Boolean

Sub Add(vNewValue As Variant)
Item_Pointer = Item_Pointer + 1
If Item_Init Then
ReDim Preserve Item_Data(Item_Pointer)
Else
ReDim Item_Data(Item_Pointer)
Item_Init = True
End If
Item_Data(Item_Pointer) = vNewValue
End Sub

Sub Remove(Index As Long)
Dim Imsi_Data() As Variant
Dim i As Long
Dim j As Long

If Item_Init Then
ReDim Imsi_Data(Item_Pointer - 1)
j = 1
For i = 1 To UBound(Item_Data)
If i <> Index Then
Imsi_Data(j) = Item_Data(i)
j = j + 1
End If
Next

Item_Pointer = Item_Pointer - 1
ReDim Item_Data(1 To Item_Pointer)

For i = 1 To UBound(Imsi_Data)
Item_Data(i) = Imsi_Data(i)
Next
End If
End Sub

Function Count() As Long
Count = Item_Pointer
End Function

Function Item(Index As Long) As Variant
If Item_Init Then
If Index < 1 Or Index > Item_Pointer Then
Err.Raise ERR_LIST_BEYOND_BOUNDS, "CList", "범위를 벗어났습니다"
Else
Item = Item_Data(Index)
End If
Else
Err.Raise ERR_LIST_NOT_INITIALIZED, "CList", "List()가 초기화되지 않았습니다"
End If
End Function

Private Sub Class_Initialize()
Item_Pointer = 0
Item_Init = False
Erase Item_Data()
End Sub

Private Sub Class_Terminate()
Erase Item_Data()
End Sub

다음은 위의 클래스모듈을 사용한 예이다.
Option Explicit

Sub demo_CListClass()
Dim lstDemo As New CList

'// 6개의 데이터를 입력한다
lstDemo.Add "Tumin"
lstDemo.Add "Lenski"
lstDemo.Add "Davis"
lstDemo.Add "Moore"
lstDemo.Add "Ogburn"
lstDemo.Add "Simmel"

'// 2번째 입력한 데이터를 삭제한다
lstDemo.Remove 2

Dim i As Long

'// 리스트의 처음부터 끝까지 보여준다
For i = 1 To lstDemo.Count
Debug.Print lstDemo.Item(i)
Next

Set lstDemo = Nothing
End Sub


[관련포스팅]없으면 빌려쓰는 ArrayList

구글앱스크립트로 실시간 데이터 업데이트하기 오피스/VBA/Office.JS

이번에는 구글 앱스크립트로 실시간 페이지를 업데이트하는 예를 소개하고자 하는 데, 직접 만든 건 아니고(아직 그럴 짠밥이 못된다) 깃헙(Github)에 좋은 프로젝트가 하나 있어 소개한다.

이 프로젝트는 일정간격으로 TheSimpsons Quote API(심슨이 들려주는 격언, https://thesimpsonsquoteapi.glitch.me/)에서 격언과 이미지를 가져오는 것이다. 그 결과물은 다음과 같다.

깃허브에 가면 소스코드를 볼 수 있는 데, 프로젝트는 매우 단촐하다. 서버사이드 코드 Code.gs와 클라이언트 사이드 Index.html(자바스크립트 포함)가 전부이다(이런 간단명료한 거 좋아한다) 클라이언트 사이드 소스코드에서
  • google.script.run.withSuccessHandler는 비동기 클라이언트 사이드 API인데, 서버 사이드 함수를 사용가능하게 해준다.
  • setInterval()함수는 업데이트 주기를 설정하는 클라이언트 사이드 API이다.

  • 그런데 직접 코드를 가져와 몇 번 돌려보고, 저자의 데모를 돌려보니 심슨격언을 가져오지 못한다. 이는 구글웹앱에 많은 HTTP 리퀘스트를 날리기 때문이다. 무료의 한계이다. (참고: Quotas for Google Services, https://developers.google.com/apps-script/guides/services/quotas)

    [Code.gs 서버사이드]
    function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('Index').setTitle('Realtime Data');
    }

    function randomQuotes() {
    var baseURL = 'https://thesimpsonsquoteapi.glitch.me/quotes';
    var quotesData = UrlFetchApp.fetch(baseURL, { muteHttpExceptions: true });
    var quote;
    var imageURL;
    if (quotesData.getResponseCode() == 200 || quotesData.getResponseCode() == 201) {
    var response = quotesData.getContentText();
    var data = JSON.parse(response)[0];
    quote = data["quote"];
    imageURL = data["image"];
    } else {
    quote = 'Random Quote Generator is broken!';
    imageURL = 'https://cdn.shopify.com/s/files/1/1061/1924/products/Sad_Face_Emoji_large.png?v=1480481055';
    }
    var randomQuote = {
    "quote": quote,
    "imageTag": '<img class="responsive-img" src="' + imageURL + '">'
    }
    return randomQuote;
    }

    function getTime() {
    var now = new Date();
    return now;
    }

    [Index.html내 자바스크립트 클라이언트 사이드]
    <script>
    // 격언 업데이트
    function onSuccess1(quotedata) {
    var quoteactual = document.getElementById('quote');
    quoteactual.innerhtml = quotedata.quote;
    var quoteimg = document.getElementById('quoteImage');
    quoteimg.innerhtml = quotedata.imagetag;
    }

    setInterval(function() {
    console.log("getting quote...")
    google.script.run.withSuccessHandler(onsuccess1).randomQuotes();
    }, 10000);
    </script>

    <script>
    // 시간 업데이트
    function onSuccess2(now) {
    var div = document.getElementById('time');
    var today = new Date();
    var time = today.getHours() + " : " + today.getMinutes() + " : " + today.getSeconds();
    div.innerhtml = time;
    }

    setInterval(function() {
    console.log("getting time...")
    google.script.run.withSuccessHandler(onsuccess2).getTime();
    }, 1000);
    </script>

    VBA로 만드는 알고리즘-큐 오피스/VBA/Office.JS

    큐는 선입선출형 알고리즘이다. 즉 먼저 들어온 넘이 먼저 나가는 구조이다. 극장이든, 식당이든, 코스트코이든 줄을 서서 기다리다 순서대로 입장하는 구조이다.

    매일 우리가 만나는 큐는 키보드나 프로그램큐이다. 예전에 컴퓨터성능이 거지같았을 때 급한 마음에 입력한 입력한 글자들이 나중에 한꺼번에 입력되거나, 이것저것 클릭한 프로그램들이 당장 안 보이고 대충 실행한 순서대로 화면에 뜨는 기적을 보았을 것이다. 사용자의 입력을 기억해두는 일을 하는 것이 큐이다. 키보드에 글자를 입력하면 키값이 큐에 저장되고 윈도는 여유있을 때마다 키값을 읽어와 화면에 렌더링하는 것이다.

    보통 큐에 데이터를 입력하는 것을 'Enqueue'라고 하고, 데이터를 꺼내는 것을 'Dequeue'라고 한다. 다음은 VBA로 만들어 본 큐이다. 두 개의 클래스모듈을 사용하는 데, 하나는 큐에 들어갈 데이터를 정의한 클래스 QueueItem, 나머지 하나는 큐를 구현한 Queue클래스이다.
    [QueueItem.cls]
    Public nItem As QueueItem
    Public Value As Variant

    Private Sub Class_Initialize()
    Set nItem = Nothing
    End Sub

    Private Sub Class_Terminate()
    Set nItem = Nothing
    End Sub
    [Queue.cls]
    '// 큐의 머리와 꼬리를 가리키는 멤버변수 선언
    Dim qFront As QueueItem
    Dim qRear As QueueItem

    Public Sub Enqueue(newItem As Variant)
    '// 큐에 입력하는 메서드
    Dim qNew As New QueueItem

    qNew.Value = newItem

    '// 큐가 비어있다면 qNew는 '머리=꼬리'이고
    '// 비어 있지 않다면 이전 꼬리에 qNew를 입력하고
    '// qRear에 qNew를 지정한다
    If IsEmpty Then
    Set qFront = qNew
    Set qRear = qNew
    Else
    Set qRear.nItem = qNew
    Set qRear = qNew
    End If
    End Sub

    Public Function Dequeue() As Variant
    '// 먼저 큐가 비어있는 지 확인하고
    If IsEmpty Then
    Dequeue = Null
    Else '// 비어 있지 않다면 일단 큐의 머리값을 꺼낸다
    Dequeue = qFront.Value

    '// 그러나 큐에 데이터가 1개여서
    '// 머리=꼬리인 상태라면 앞서 데이터를 꺼낸 상황이라
    '// 이제 큐가 빈 상태가 된다.
    '// 그래서 머리,꼬리는 Nothing으로 만든다
    If qFront Is qRear Then
    Set qFront = Nothing
    Set qRear = Nothing
    Else '// 그러나 큐에 데이터가 1개 이상이어서
    '// 머리 != 꼬리인 상황이라면
    '// 머리의 다음 데이터가 머리가 된다
    Set qFront = qFront.pItem
    End If
    End If
    End Function

    Property Get IsEmpty() As Boolean
    '// 머리와 꼬리가 모두 Nothing 이면
    '// 비어 있다는 의미이다
    IsEmpty = ((qFront Is Nothing) And (qRear Is Nothing))
    End Property

    Property Get Peek() As Variant
    '// Dequeue를 하지 않고 머리 값을 얻는다
    If Not IsEmpty Then Peek = qFront.Value
    End Property

    Private Sub Class_Initialize()
    Set qFront = Nothing
    Set qRear = Nothing
    End Sub

    Private Sub Class_Terminate()
    Set qFront = Nothing
    Set qRear = Nothing
    End Sub

    다음은 큐를 사용하는 데모이다.
    [QueueTest]
    Dim myQueue As New Queue

    With myQueue
    .Enqueue "A queue is "
    .Enqueue "a useful data structure "
    .Enqueue "in programming. "
    .Enqueue "It is similar to the ticket queue "
    .Enqueue "outside a cinema hall, "
    .Enqueue "where the first person "
    .Enqueue "entering the queue "
    .Enqueue "is the first person "
    .Enqueue "who gets the ticket."

    Debug.Print ">> Get the value of the front of queue :", .Peek

    Do While Not .IsEmpty
    Debug.Print .Dequeue()
    Loop

    Debug.Print ">> Is myQueue empty?", .IsEmpty
    End With

    신입직원 인사기록을 위한 작은 예제 오피스/VBA/Office.JS

    직원을 채용하면 인사담당자는 신입직원에 대한 기록을 만들게 된다. 이번에는 구글앱스크립트와 구글시트를 이용하여 이름, 직위, 아이디(#사번)를 워크시트에 입력하는 앱스크립트이다. 사실 이건 전혀 실무에 도움이 되지 않는 아주 초보적인 수준으로 학습을 위한 용도이다(그냥 시트에 손으로 입력하는 게 더 낫다)

    function addEmployee() {

    var ui = SpreadsheetApp.getUi();
    var ss = SpreadsheetApp.getActiveSpreadsheet();

    ss.setActiveSheet(ss.getSheetByName("Employee"));

    var answer1 = ui.prompt(
    '신규직원추가',
    '이름을 입력하세요',
    ui.ButtonSet.OK);
    var btnEmployeeName = answer1.getSelectedButton();
    var txtEmployeeName = answer1.getResponseText();

    if (btnEmployeeName == ui.Button.OK) {
    var answer2 = ui.prompt(
    '직책을 입력하세요',
    '직책:',
    ui.ButtonSet.OK_CANCEL);
    var btnEmployeePosition = answer2.getSelectedButton();
    var txtEmployeePosition = answer2.getResponseText();

    if (btnEmployeePosition == ui.Button.OK) {
    var answer3 = ui.prompt(
    '사번을 입력하세요',
    '사번(ID#):',
    ui.ButtonSet.OK_CANCEL);
    var btnEmployeeId = answer3.getSelectedButton();
    var txtEmployeeId = answer3.getResponseText();
    }

    if (btnEmployeeId == ui.Button.OK) {
    var sheet = SpreadsheetApp.getActiveSheet();
    sheet.appendRow([txtEmployeeName, txtEmployeePosition, txtEmployeeId]);

    } else if (btnEmployeeId == ui.Button.CANCEL) {
    ui.alert('작업 취소');
    }

    } else if (btnEmployeePosition == ui.Button.CANCEL) {
    ui.alert('작업 취소');

    } else if (btnEmployeeName == ui.Button.CLOSE) {
    ui.alert('작업 취소');
    }
    }

    VBA로 만드는 알고리즘-스택 오피스/VBA/Office.JS

    토지라는 물건을 하나 두고, 상업용,공업용,주거용,농업용 토지 등 우리는 여러 가지로 구분하여 사용하듯이, '메모리'라는 공간을 두고 필요에 따라 여러 알고리즘으로 활용한다. 스택,큐,리스트(링크드),트리 등등. 현대의 프로그래밍언어는 이런 알고리즘을 아예 지원을 해주지만, 예전에는 직접 만들어 사용하였다. 그래서 이런 알고리즘은 학교에서 배우거나 프로그래머 면접용으로 사용되거나 한다. 이런 알고리즘을 직접 만들 필요는 많이 줄었지만, 기초에 해당하는 것이라 알아둘 필요는 있어 보인다.

    스택(Stack)은 끝이 막혀있는 구조로 나중에 입력(push)된 항목이 먼저 나오는(pop) 후입선출형 알고리즘이다. 데이터를 입력하는 것을 PUSH라고 하고, 데이터를 꺼내는 것을 POP이라고 한다. POP을 하면 해당 항목은 스택에서 제거된다. 위의 그림에 '1'을 PUSH하고 , 다음에 '2'를 PUSH한다. 이제 POP을 하면 '2'가 스택에서 나오고 '1'만 남는다.

    프로그래밍을 하면서 만나는 스택은 재귀적 호출 함수를 만드는 경우이다. 함수가 자신을 다시 호출하는 구조인데, 호출할때마자 메모리주소를 스택에 쌓아두었다가, 연산이 끝나면 스택에 저장된 함수의 주소를 메모리에서 풀어주게 된다. 즉 마지막에 입력된 함수주소가 먼저 꺼내지는 것이다.


    VBA에서 스택을 구현하려면 클래스모듈을 사용하는 데, 이번 예에서는 두 개의 클래스를 만든다. 하나는 스택에 들어갈 항목을 정의하는 StackItem클래스이고, 나머지 하나는 스택의 메서드 PUSH, POP을 구현한 Stack클래스이다.
    [StackItem.cls]
    Public Value As Variant '// 항목을 저장할 멤버변수
    Public pItem As StackItem '// 이전항목을 위한 변수

    Private Sub Class_Initialize()
    '// 클래스 생성자로서 처음에 빈 스택이므로 Nothing이다
    '// PUSH를 할때 실행된다
    Set pItem = Nothing
    End Sub

    Private Sub Class_Terminate()
    '// 클래스 소멸자, POP을 할 때 실행된다
    Set pItem = Nothing
    End Sub
    다음은 본격적인 스택클래스이다
    [Stack.cls]
    Dim Top As StackItem '// 마지막 입력된 항목을 가리키는 StackItem 이다.

    Public Function Pop() As Variant
    '// 스택에서 마지막 입력된 항목을 꺼내는 메서드함수이다
    If Not IsEmpty Then
    '// 스택이 비어 있지 않다면 마지막 항목을 돌려준다
    '// 그리고 다음항목을 Top으로 지정한다
    Pop = Top.Value
    Set Top = Top.pItem
    End If
    End Function

    Public Sub Push(ByVal nItem As Variant)
    '// 스택에 데이터를 입력하는 PUSH 메서드이다.
    Dim NewTop As New StackItem

    NewTop.Value = nItem
    '// 기존 TOP을 예전항목으로 돌리고, 새 항목을
    '// TOP으로 설정한다
    Set NewTop.pItem = Top
    Set Top = NewTop
    End Sub

    '// 클래스모듈에서만 작성하는 프로퍼티 메서드이다
    '// 멤버변수값을 돌려주는 프로퍼티 메서드는 Get이고
    '// 멤버변수값을 설정하는 프로퍼티 메서드는 Set이다.
    Property Get GetTop() As Variant
    '// TOP을 돌려준다
    If IsEmpty Then
    GetTop = Null
    Else
    GetTop = Top.Value
    End If
    End Property

    Property Get IsEmpty() As Boolean
    '// 스택이 비어 있는 지 여부를 알려준다
    IsEmpty = (Top Is Nothing)
    End Property

    Private Sub Class_Initialize()
    Set Top = Nothing
    End Sub

    Private Sub Class_Terminate()
    Set Top = Nothing
    End Sub
    다음에는 이를 사용하는 데모이다.

    A stack is a useful data structure in programming. It is just like a pile of plates kept on top of each other.
        Dim myStack As New Stack

    myStack.Push "kept on top of each other."
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "a pile of plates"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "It is just like"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "in programming."
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "a useful data structure"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "is"
    Debug.Print "Current Top Item :", myStack.GetTop

    myStack.Push "A stack"
    Debug.Print "Current Top Item :", myStack.GetTop

    Debug.Print
    Debug.Print "--------------------------------------"
    Debug.Print "It's time to pop out"
    Do While Not myStack.IsEmpty
    Debug.Print "Item just popped :", myStack.Pop()
    Debug.Print "Current Top Item :", myStack.GetTop
    Debug.Print
    Loop

    환율, 구글시트 그리고 앱스크립트 오피스/VBA/Office.JS

    사람이 아닌 소프트웨어를 이용하여 각종 정보를 가져오는 방법중 가장 쉬운 방법은 구글시트 함수를 이용하는 것이다. 그리고 이렇게 얻어온 정보를 단순히 시트를 열어보는 것으로 끝나는 게 아니라, 일정 간격으로 메일로 보내주는 작업까지 생각해볼 수 있다.

    이번 예에서는 구글시트에서 importxml(url, xpath_query)함수를 사용하여 네이버 환율정보를 가져오는 문서를 구성하고 정기적으로 메일로 전송하는 것이다.

    첫 번째 단계는 환율정보를 가져오는 시트를 구성하는 것인데, importxml()를 사용한다. importxml()에 넘겨줄 정보는 url과 xpath_query이다. 네이버 환율정보를 가져올 url과 xpath는 다음과 같다.
    가령 미국달러환율이라면 'http://finance~FX_' 뒤에 'USDKRW'를 붙이면 된다.

    http://finance.naver.com/marketindex/exchangeDetail.nhn?marketindexCd=FX_USDKRW

    그리고 xpath는 다음과 같다.

    '//*[@id="content"]/div[2]/table[1]/tbody/tr/td[1]'


    이 정보는 크롬브라우저나 파이어폭스브라우저의 개발자도구에서 알아낼 수 있다. 이렇게 간단하게 시트를 구성하면 열어볼때 마다 최근 환율을 가져오게 된다.

    두 번째 단계는 시트의 환율정보를 읽어와서 메일로 보내주는 것이다. 시트의 메뉴 [Tools]-[<>Script editor]를 클릭하여 다음과 같은 앱스크립트를 작성할 것이다.
    function myFunction() {
    var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('FX');
    var rng = sheet.getRange(6, 1, sheet.getLastRow(), 2).getValues();
    var date_today = new Date();

    var email_body = "날짜: "
    + Utilities.formatDate(date_today, Session.getScriptTimeZone(), "yyyy-MM-dd");

    for(var r=0;r<rng.length;r++){
    if(rng[r][0]!=='')
    email_body = email_body + '<br>'+rng[r][0]+' : '+ rng[r][1];
    }

    var email_to = "your mail@address";
    var email_subject = "NAVER Finance 환율";
    MailApp.sendEmail({
    to: email_to,
    subject: email_subject,
    htmlBody: email_body,
    });
    }
    세 번째 단계는 정기적으로 메일을 보낼 수 있도록 이벤트를 만든다. [Edit]-[Current project's triggers] 또는 도구모음의 아이콘을 클릭하여 이벤트 트리거를 만든다. 즉 예약작업을 걸어 두 번째 단계의 코드를 정기적으로 실행하는 것이다.


    Google Apps Script와 OfficeJs 오피스/VBA/Office.JS



    Google Apps Script라는 물건이 있는 데, 게을러서 북마크만 해두고 있다가 묵은지마냥 꺼내본다. 예전에는 단순한 VBA 대용품 정도로만 생각했는데, G Suite에 속한 제품을 위한 강력한 업무애플리케이션 개발환경이다.
    다음은 Google Apps Script 예이다.
    여기서도 Hello, World는 계속된다.
    /**
    * Creates a Google Doc and sends an email to the current user with a link to the doc.
    */
    function createAndSendDocument() {
    // Create a new Google Doc named 'Hello, world!'
    var doc = DocumentApp.create('Hello, world!');

    // Access the body of the document, then add a paragraph.
    doc.getBody().appendParagraph('This document was created by Google Apps Script.');

    // Get the URL of the document.
    var url = doc.getUrl();

    // Get the email address of the active user - that's you.
    var email = Session.getActiveUser().getEmail();

    // Get the name of the document to use as an email subject line.
    var subject = doc.getName();

    // Append a new string to the "url" variable to use as an email body.
    var body = 'Link to your doc: ' + url;

    // Send yourself an email with a link to the document.
    GmailApp.sendEmail(email, subject, body);
    }
    G Suite에 해당하는 것이 MS-Office365인데, MS의 현금상자인 만큼 MS도 머신러닝을 이용한 기능을 추가하는 등 많은 공을 들이고 있다. 그런데 Google Apps Script에 해당하는 업무애플리케이션 개발환경은 VBA가 아니다. OfficeJS API라는 물건인데, 이것 역시 자바스크립트 기반이다.



    호기심으로 몇 개의 토이수준 OfficeJS 앱을 만들어 보았는 데, 하다보면 자괴감이 든다. 'VBA라는 편리한 물건을 놔두고 왜 이 짓을 하는 거지?' 복잡한 개발환경 셋팅을 마치고 고인물 VBE 같은 전문개발툴도 없어 vscode와 같은 텍스트에디터로 작업을 한다. VBA로 할 수 있는 작업을 자바스크립트로 만드는 게 의미가 있나 싶었다(기껏 제공하는 게 'Script Lab'정도이다)

    '중앙집중적인 배포'라는 대의명분외에는 아무 장점이 안보인다. 그냥 자바스크립트를 이용한 웹개발을 오피스용으로 돌려놓은 것이다. OfficeJS에서 앵귤러,리액트,뷰,제이쿼리등을 사용할 수 있는 걸 보면, 전혀 새로운 것이 아닌 웹개발을 위한 자원을 오피스개발로 돌려놓은 것임을 알 수 있다. 웹보다 짠밥이 높은 오피스에 대세인 웹개발을 갖다 붙인 것이다. 덕분에 웹용으로 개발된 여러 소스를 응용하여 사용할 수 있을 것으로 보인다. 가령 웹크롤링으로 데이터를 가져오는 등...

    Google Apps Script은 MS의 OfficeJS보단 나아 보인다. 구글답게 공돌이스런 미적감각(비하아님)을 가진 개발환경(script.google.com) 을 제공해준다. 이것 역시 구글오피스의 제품을 커스터마이징하여 비지니스 애플리케이션으로 이용하는 게 주된 목적이다. 그런데 오피스제품말고 웹사이트도 만들 수 있다. 특히 sites.google.com에 붙이면 더욱 그럴 듯하다. 별도의 웹서버를 장만할 필요없이 웹프로그램을 돌릴 수 있다. 웹프로그래밍을 위해 호스팅등을 사용할 필요가 없어 보인다(쟁고django로 웹프로그램 만들려다가 웹서버세팅에 진을 빼버린 걸 생각하면 이건 신세계이다. 오로지 프로그래밍만 신경쓰면 되는 것이니...)


    Google Apps Script와 OfficeJs간의 몇 가지 공통점을 나름 느낀대로 써보았는데, 가장 큰 공통점은 둘다 그닥 인기가 없어 보인다. RPA 시대에 나름 중요한 업무용 앱 개발수단으로 보이는데, 많은 전문개발자들의 관심을 받지 못하는 듯 하다. 이럴 바엔 MS는 오피스 스크립팅 언어로 파이썬이나 돌아가게 해주는 게 나아 보이는 데..아니면 쓸데없는 OfficeJs말고 일렉트론같은 라이브러리를 이용한 스탠드얼론 제품을 만들 수 있는 자바스크립팅을 해주는 게 나을 것 같다.

    망해라 django, 아니지 흥해라 django 파이썬/쟁고

    프로그래밍이 생업이 아니다보니까 뭘 새로 해볼려면 항상 초보자이다. django를 새로 셋팅하려는 데, 까먹은 게 넘 많다. 설치는 그나마 쉽다. 각종 웹서버 설정이 골치 아프다. 개발서버는 그나마 나은데, 실제 서버에서 하면 부닺치는 문제가 많다.

    병아리반 학생이 중딩반가서 고생하는 꼴이다. 이러다 보니 django말고 flask, fastapi 등을 해볼까 하는 생각도 든다. 경험상 안된다고 돌아가려고 해봐야 좋을 꼴을 본 적이 없는 것 같다. 희망사항이지만 워드프레스와 같은 CMS에 필요한 앱을 생성하고 코딩하는 호스팅이 어디 있을 것 같은데... 그래서 장고 호스팅 업체를 검색해보았다.
    Top 6 Django Compatible Hosting Services를 읽어보니 디지털오션이 괜찮아 보인다. 다음 번에 이짓을 또한다면 여기로 해봐야 겠다.

    호스팅은 나중에 하고 어드민 화면의 CSS를 못읽어들이는 문제가 발생하였다. 일단 다음 파일에서 static에 대한 alias와 디렉토리를 만든다
    [/etc/httpd/conf.d/vhost.conf]
    <VirtualHost *:80>
    WSGIScriptAlias / /opt/rpa/rpa/wsgi.py
    WSGIDaemonProcess rpa python-path=/opt/rpa/rpa
    <Directory /opt/rpa/rpa>
    <Files wsgi.py>
    #Order allow, deny
    #Allow from all
    Require all granted
    </Files>
    </Directory>
    Alias /static/ /opt/rpa/static/
    <Directory /opt/rpa/static>
    #Order allow,deny
    #Allow from all
    Require all granted
    </Directory>

    </VirtualHost>

    [etc/httpd/conf/httpd.conf]
    Alias /static/ /opt/rpa/static

    [settings.py]
    ...
    ALLOWED_HOSTS = ['*']
    ...
    STATIC_ROOT = os.path.join(BASE_DIR, "static")

    [wsgi.py]
    import os
    import sys

    from django.core.wsgi import get_wsgi_application

    path='/opt/rpa'
    if path not in sys.path:
    sys.path.append(path)

    os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rpa.settings')
    application = get_wsgi_application()
    그리고 settings.py에서 STATIC_ROOT 설정하고 , manage.py collectstatic를 실행한다. 자세한 내용은 How to setup static files in Django에서 볼 수 있다.

    기타 실서버에서 설정하다보니 걸리는 몇 가지 애로사항은 슈퍼유저를 만들려니 sqlite디비 권한문제가 있다. attempt to write a readonly database, unable to open database file같은 에러 메시지를 만나기도 한다. 파일이나 폴더에 대한 권한 문제인데, 사용자 apache에게 권한을 부여해야 한다.
    chown apache:apache /opt/rpa
    chown apache:apache /opt/rpa/db.sqlite3


    웹 개발이 대세인 시대라 어쩔 수 없이 내가 필요해서 하지만, 데스크탑 앱 개발이 훨씬 좋았다.

    ndarray의 head, tail 구하기 파이썬/쟁고


    데이터프레임을 사용하는 경우 데이터의 앞뒤를 head와 tail을 사용하여 구하는 데, ndarray타입인 경우 head, tail이 따로 없다. 이런 경우 슬라이싱 연산자를 사용한다.
    import numpy as np

    # 데이터 머리부터 두 개의 데이터를 가져온다
    head = np.array((1,2,3,4,5))[:2]

    # 중간 3,4번째 데이터를 가져온다
    body = np.array((1,2,3,4,5))[2:4]

    # 꼬리에서 두 개의 데이터를 가져온다
    tail = np.array((1,2,3,4,5))[-2:]
    print(head)
    print(body)
    print(tail)

    # 슬라이싱 말고 아래와 같이 제한적이긴 하지만 일부 데이터를 가져 오는 데
    # 아래의 *는 나머지를 의미한다
    head, body, *tail = np.array((1,2,3,4,5))
    print(head)
    print(body)
    print(tail)


    게으른 자를 위한 코딩 학습법, 손이 게으른 거지, 머리가 게으른 게 아니다. C/C++

    프로그래밍 연습은 컴퓨터로 하지만, 반드시 그래야 하는 것은 아니라고 생각한다. 괜히 부팅하고 나서, 포털의 연애뉴스 분석하기, 스팸메일이나 구독메일이나 가득찬 메일상자 확인하기, 배그나 롤의 세계로 가서 '자식들~아직도 이러고 있네~' 하며 총알로 안부 챙기기 등 시간(낭비)여행을 하게 되기 때문이다.
    이런 유혹에 흔들리지 않고, 아까운 전기 낭비하지 않기 위해, 프로그래밍 퀴즈를 풀어 보는 것도 좋다. 뇌를 CPU로 다운그레이드하여 결과를 예상해보는 것도 좋을 것이다. PC없는 코딩연습이라는 관점에서 동질적인 방법인데, 예전에 종이에 코딩을 해보는 연습을 추천한 것도 보았는데, 사실 그런 정성을 들이는 자는 절대 게으른 자는 아니다(좋은 방법이지만 오래할 방법은 아니다)




    퀴즈중에 위의 같은 문제가 나왔는데, 힌트는 printf가 화면에 출력하는 것만 하는 게 아니라 출력한 문자의 갯수를 돌려주는 것이다. 언제 이런 기능이 생겼니? 그래서 정답은 비디오에...

    개눈에는 똥만 보인다 파이썬/쟁고


    요 며칠 해외뉴스를 크롤링하는 코드를 만지다 보니 환율도 욕심이 생겨 네이버 파이낸스에서 환율을 스크래핑해볼까 했다. 그런데 파이썬에는 환율정보를 갖다 바치는 라이브러리 forex-python이 있다. 한 가지만 생각하다 멍청하게 생고생할 뻔했다. 암튼 텔레그램봇에 환율조회 추가 완료!
    그런데 이거 간단한 라이브러리라 사용하기 어렵진 않은 데, 환율을 가져보는 속도가 좀 느린 단점이...그나저나 안 본 사이에 캐나다환율 많이 올랐네. 젠장~ 은행넘들 수수료 주기 아까워 미루다 보니...
    01 from datetime import datetime
    02 from forex_python.converter import get_rate

    03 def get_fx():
    04 t = datetime.today()
    05 currency = ['USD', 'JPY', 'EUR', 'GBP', 'CAD', 'HKD', 'AUD', 'SGD']
    06 fx=[]
    07 for c in currency:
    08 fx.append('{0} {1:0.2f}'.format(c, get_rate(c, 'KRW', t)))
    09 return fx

    10 fx=get_fx()

    11 for f in fx:
    12 print(f)
    06~08 사이의 행은 fx=['{0} {1:0.2f}'.format(c, get_rate(c, 'KRW', t)) for c in currency] 와 같이 한 개의 행으로 대체가능하다.

    [고전] The Tao of Programming

    위(魏)나라의 조정에 한 프로그래머가 있었다. 위후(魏候)가 프로그래머에게 묻기를

    " 회계 프로그램과 운영체제중에 설계하기 쉬운 것은 어느 쪽이요? "
    " 운영체제이옵니다."
    하고 프로그래머가 답했다. 위후는 믿을 수 없다는 표정으로 반문하였다.

    "어찌 회계 프로그램처럼 하찮은 것이 운영체제의 복잡함을 능가한다는 말이요? "

    "그렇지 않사옵니다. 회계 프로그램을 설계할 때는 프로그래머가 서로 다른 생각을 지닌 사람들을 조율해야만 하옵니다. 회계 프로그램이 어떻게 작동해야 하며, 보고서는 어떤 양식으로 출력되어야 하며, 세법에는 어느 정도로 충실해야 하는지 각양각색으로 떠들기 마련이옵니다.

    반면에 운영체제의 외양에는 아무도 신경쓰지 않사옵니다. 운영체제를 설계할 때는 프로그래머는 기계와 아이디어의 가장 단순한 조화만 추구하면 되옵나이다. 이것이 운영체제가 설계하기 쉬운 까닭이옵니다. 옛말에는 이를 일컬어 사공이 많으면 배가 산으로 간다고 하옵니다.
    "

    크게 감탄한 위후가 미소를 지으며 다른 질문을 던졌다.

    "그렇구려, 그런데 어느 쪽이 더 디버깅하기 쉽소? "

    프로그래머는 아무런 답도 하지 못했다.

    VBA에서 람다함수 만들기 오피스/VBA/Office.JS

    블로그 단골손님, 우왕님의 천재적인 아이디어에 힘입어 VBA에서 람다함수를 만들어 보았다. 진짜 람다함수는 아니고, 엑셀의 이름기능과 VBA의 EVALUATE함수를 이용한 것이다.

    이번에 만드는 람다함수는 간단하다. 변수X와 변수Y 사이의 난수값을 생성하는 RANDBETWEEN()함수를 이용할 것이다.
    먼저 그림과 같이 엑셀에서 미리 이름을 만들어 둔다(코드로 만들수도 있지만)

    LAMBDA_RANDXY는 람다함수의 이름이라고 생각하고 수식 =RANDBETWEEN(ParamX, ParamY)은 람다함수의 본체이다.
    그리고 ParamX와 ParamY는 변수X, 변수Y이다. ParamX와 ParamY의 값을 람다함수 LAMBDA_RANDXY가 받아서 RANDBETWEEN(ParamX, ParamY)에 전달한다고 생각하면 된다.

    다음은 이 세 개의 이름을 가지고 람다함수 비스무리한 것을 구현한 것이다(내 눈을 바라봐~ 넌 람다함수가 보이고~)
    1    Dim nmLambda As Name
    2 Dim paramX As Name
    3 Dim paramY As Name

    4 Set nmLambda = ThisWorkbook.Names("LAMBDA_RANDXY")
    5 Set paramX = ThisWorkbook.Names("ParamX")
    6 Set paramY = ThisWorkbook.Names("ParamY")

    7 paramX.Value = 12
    8 paramY.Value = 20

    9 Debug.Print Evaluate(nmLambda.Value)
    1~3번 행은 워크북에 저장된 이름을 저장할 개체변수이다.
    4~6번 행은 선언한 개체변수에 이름을 할당한다.
    7~8번 행에서 변수X, 변수Y에 해당하는 ParamX와 ParamY의 값을 변경하다.
    9번 행에서 Evaluate()함수를 이용하여 LAMBDA_RANDXY의 값, =RANDBETWEEN(ParamX, ParamY)를 계산한다.

    이번에는 스칼라 값이 아니라 배열을 다루는 람다함수를 만들어 본다. 함수를 만들기에 앞서 먼저 풀어야할 썰이 하나 있다.
    엑셀의 배열은 컬럼은 컴마(,)로 구분하고 , 행은 세미콜론(;)으로 구분하여 만든다. 가령 아래와 같은 3x3 행렬이 있다고 하자.

    엑셀은 내부적으로 이것을 {1,4,7;2,5,8;3,6,9} 로 처리한다.
    그래서 VBA의 배열을 람다함수에 던져주려면 위와 같은 형식의 문자열을 만들어야 한다.

    다음은 SUMPRODUCT()함수에 배열을 던져주고 계산결과를 받아오는 람다함수이다.
    01    Dim nmLambda As Name
    02 Dim arrX As Name
    03 Dim arrY As Name
    04 Dim x(4), y(4)

    05 Set nmLambda = ThisWorkbook.Names("LAMBDA_SUMP")
    06 Set arrX = ThisWorkbook.Names("arrX")
    07 Set arrY = ThisWorkbook.Names("arrY")

    08 x(0) = 1990
    09 x(1) = 29290
    10 x(2) = 36990
    11 x(3) = 9790
    12 x(4) = 8590

    13 y(0) = 12490
    14 y(1) = 22990
    15 y(2) = 8990
    16 y(3) = 8190
    17 y(4) = 10990

    18 arrX.Value = "={" & Join(x, ";") & "}"
    19 arrY.Value = "={" & Join(y, ";") & "}"

    20 Debug.Print arrX.Value
    21 Debug.Print arrY.Value

    22 Debug.Print Evaluate(nmLambda.Value)
    01~04행은 변수를 선언한 부분이다. 04 행에 5칸짜리 배열이 선언되었다.

    05~07 행은 저장한 이름을 개체변수에 저장하는 것이다.
    08~ 17 행은 배열에 값을 넣는 숙달된 조교의 노가다이다. 실무에서 이런 일을 하지 않지만...
    18~19 행은 Join함수를 이용하여 배열의 각 원소를 세미콜론(;)으로 구분된 문자열로 만드는 것이다. 엑셀의 행렬 만드는 규칙을 따르는 것이다.
    20~21 행은 입력한 값을 한번 찍어 본다.
    22 행에서 드디어 저장한 배열을 가지고 SUMPRODUCT를 계산한다. 그러나 지난 번 포스팅(VBA 배열에 앙심을 품은 자)에서 언급한 바와 같이 'LAMBDA_SUMP'는 엑셀에서 사용가능한 이름개체이므로 다음과 같이

    Debug.Print [LAMBDA_SUMP]

    로 표현해도 된다. 뭔가 VBA같지 않은 이상한 결과이다.

    이걸 잘 활용하면 코드를 깔끔하게 - 나만 고칠 수 있어서 함부로 날 못짜르는 - 만들 수 있을 것 같다.

    결론은 '우왕님, 땡큐베리감사~'

    (추가)
    람다함수는 임시로 만드는 간단한 함수의 형태이므로 좀 더 완벽을 기하기 위해 혹시 같은 이름이 있다면 닥치고(On Error Resume Next) 지우고(Names(~~~).Delete), 만들어(Names.Add) 계산한 후([LAMBDA_SUMP]), 다시 증거를 인멸(Names(~~~).Delete)하는 완전범죄를 해본다. 변수도 전혀 사용하지 않는다
        On Error Resume Next
    With ThisWorkbook
    .Names("LAMBDA_SUMP").Delete
    .Names("arrX").Delete
    .Names("arrY").Delete

    .Names.Add "LAMBDA_SUMP", "=SUMPRODUCT(arrX,arrY)"
    .Names.Add "arrX", Sheet2.Range("J5:J7")
    .Names.Add "arrY", Sheet2.Range("K5:K7")

    Debug.Print [LAMBDA_SUMP]

    .Names("LAMBDA_SUMP").Delete
    .Names("arrX").Delete
    .Names("arrY").Delete
    End With
    물론 이것이 파이썬의 그것에 비해 부족하다. filter(), map(), reduce()와 같은 함수에 사용되는 파이썬을 따라가기는 역부족이다. 그러나 VBA에서도 이를 함수로 만들어 볼 수 있다(이건 준비중...)

    아리랑뉴스 시간입니다. 파이썬/쟁고

    이글루스 어느 블로거가 아리랑 뉴스를 훔쳐 가고 있다는 속보입니다. 이 블로거는 허접한 실력으로 아리랑 뉴스의 헤드라인 뉴스 링크와 제목을 훔쳐갔다는 소식입니다(이 뭐 하는 짓인가?...현타가 오는)

    from urllib.request import urlopen
    from bs4 import BeautifulSoup
    import urllib

    html = "http://www.arirang.com/Index.asp?sys_lang=Eng"

    def link(html):
    req = urllib.request.Request(html, headers={'User-Agent': 'Mozilla/5.0'})
    response = urllib.request.urlopen(req).read()
    return response

    bs0bj = BeautifulSoup(link(html), "html.parser")
    article_li = bs0bj.find('article', {'id':'ai_lastestnews'}).find('div',{'class':'ai_slide'})
    article_link = article_li.find_all('a')
    for a in article_link:
    print('http://www.arirang.com{0}'.format(a.get('href')) ,a.text.strip())


    암튼 그렇답니다. 다음은 장물입니다.
    http://www.arirang.com/News/News_View.asp?nseq=248216 S. Korea, Japan to resume talks next month on export controls
    http://www.arirang.com/News/News_View.asp?nseq=248228 S. Korea's GDP to grow 2.0% in 2019, 2.3...
    http://www.arirang.com/News/News_View.asp?nseq=248227 S. Korea's ruling party, gov't, Blue Hou...
    http://www.arirang.com/News/News_View.asp?nseq=248219 Korea's 3 major indicators of industrial...
    http://www.arirang.com/News/News_View.asp?nseq=248218 Bank of Korea keeps key interest rate un...
    http://www.arirang.com/News/News_View.asp?nseq=248204 Trump makes surprise Thanksgiving trip t...
    http://www.arirang.com/News/News_View.asp?nseq=248212 S. Korea, Japan to resume talks next mon...
    http://www.arirang.com/News/News_View.asp?nseq=248203 Hong Kong denounces Trump for signing 'H...

    자바스크립트가 멈추는 날 삽질의 추억

    자바스크립트는 간혹 애매모호한 문법으로 욕도 먹지만 인터넷세상을 지배하는 언어이다. 클라이언트와 웹서버부터 일렉트론을 이용하여 vscode, atom 에디터같은 이용한 데스크톱 응용프로그램을 만드는 걸 보면 놀라울 정도이다.

    오늘도 여느 때처럼 자료를 찾아 산기슭을 어슬렁거리는 하이에나처럼 남들의 자료를 찾아다니던 중, 이미지가 뿌연 안개처럼 보이는 사이트가 신경을 긁는다. 애꿎은 애드블럭을 꺼보았지만 범인은 아니었다.

    이게 무슨 현대미술 추상화가의 작품인가?


    '그러고 보니 네이버 블로그에 이런 현상이 생기네~ 애드블럭탓인가'

    무신경하게 넘어가던 여느 날과 달리 뭔가 머리에 하나가 스친다. 얼마 전에 마우스 클릭을 막아둔 블로그 땜에 자바스크립트 실행을 막아두었는데, 네이버 블로그도 자바스크립트 실행을 막아둔 웹 사이트중 하나였다.

    자바스크립트가 멈추었더니 생각하지 못한 부작용이 생긴다. 자바스크립트를 차단한 이유는 이 블로그들이 복사를 막아 두었기 때문이다. 개발자 도구를 켜서 복사할 수 도 있는 세상인데, 이런 치사한...암튼 예전에는 이상한 블로그들이 많았다. 남의 자료를 가져다 그대로 복붙해놓고, 자신의 자료인 양 포스팅을 복사해가지 못하게 막아두는 어불성설같은 ... 지금은 그런 양심이가 이상한 블로그가 많이 보이지 않는다.

    과연 원인이 무엇인가?
  • 양심이가 이상한 블로그는 전혀 줄지 않았다. 다만 내가 예전처럼 자료를 찾지 않아, 그런 블로그를 예전만큼 보지 못했기 때문이다.
  • 양심이가 이상한 블로그는 줄었다. 텍스트가 아니라 비디오의 시대라서, 이런 일을 하는 사람들이 줄었기 때문이다.
  • 양심이가 이상한 블로그는 줄었다. 사람들의 저작권 개념이 예전과 달리 선진적으로 발전하였다.



  • 처음으로 써보는 crontab 파이썬/쟁고

    최근에 텔레그램봇을 만들면서 든 생각중 하나는 공부를 제대로 안하고 대충 했구나 하는 생각이다. 그냥 무심하게 개발관련 글들을 보았는데, 봇이 개인을 상대로 메시지를 보내는 경우도 있고, 채널에 메시지를 보내는 것이 있는 데, 미처 그런 생각을 하지 못하고 넘겨왔다. 또 많은 블로그나 정보에서 보여주는 내용이 봇의 구현인데, 속 시원한 답을 얻지 못하는 것은...

    봇이 상시적으로 서비스를 하려면 웹서버같은 물건이 필요한 거 아닌가? 이런 서비스를 하려면 무엇을 해야 하는 지 알려주는 내용이 없을 까?

    아무튼 간신히 시험삼아 시간을 뿌려주는 봇과 이를 받아주는 채널을 만들었다. 봇을 원하는 대로 만들기에 앞서 전체적인 프로세스를 체크하기 위한 목적이다.

    이제 봇을 일정시간마다 뿌려주려면 crontab을 이용해야 한다. 서버가 필요한 건 아니고, 그냥 백그라운드로 스크립트를 실행하면 되는 것이다. 사실 리눅스에서 스케줄링을 하는 도구로 cron이라는 서비스(데몬)가 해주는 걸 알고는 있었다. 그러나 실제 그걸 사용할 일이 나에게는 없었다. 이번에 알게 된 것은 스케줄에 걸어둔 프로세스에 에러가 있으면, 얘가 아무 것도 안한다는 점이다. 파이썬 스크립트에 에러가 있어도 안되고, >>를 사용하여 로그를 만드는 부분의 에러가 있어도 안된다는 점이다.
    [참고자료][CentOS] Linux 반복 예약작업(스케줄러) - Crontab

    그리고

    python my_script.py

    형식으로 하면 정상동작하는 넘이 쉘스크립트로 작성해서 실행하면 "ImportError No named module~" 에러를 낸다. 여러 가지 구글검색을 통해 결국 성공한 방법은 경로를 꼼꼼히 적어두는 것이다.
    다음은 crontab의 내용과 쉘스크립트의 내용이다.
    [root@localhost tbot]# crontab -l
    5 23 * * * /usr/bin/python3.6 /opt/tbot/teli.py >> /opt/tbot/finews.log
    [root@localhost tbot]# ./start_bot.sh
    [root@localhost tbot]# ll
    total 12
    -rw-r--r-- 1 root root 28 Nov 21 23:24 finews_11212324.log
    -rwxrwxrwx 1 root root 84 Nov 21 23:24 start_bot.sh
    -rwxr-xr-x 1 root root 337 Nov 21 11:24 teli.py
    [root@localhost tbot]# cat start_bot.sh
    /usr/bin/python3.6 /opt/tbot/teli.py >> /opt/tbot/finews_$(date +\%m\%d\%H\%M).log


    야 이넘아! 고만 보내라~나 시계 있다

    Beta Is Dead; Rock Is Dead 증권



    투자자는 위험을 기피하는 속성이 있다. 그러나 더 많은 보상이 있다면 더 큰 위험을 수용할 수 있다. 위험이 작으면 작은 보상에도 만족한다. 그러면 위험을 어떻게 측정하나? 마코위츠와 샤프가 내린 정의는 변동성이다. 주식이나 포트폴리오의 가치가 오르락 내리락 하는 정도가 변동성이고 , 변동성이 크면 위험이 큰 것이다. 구체적으로 변동성은 표준편차로 측정하거나 베타로 측정한다.

    흔히 위험과 수익률의 관계를 그래프로 그리면 우상향할 것으로 생각한다. 왜냐하면 우리의 머리속엔 ‘High Risk, High Return’이라는 명제가 박혀 있기 때문이다. 그러나 실제 아무런 상관관계를 보이질 않는다. 변동성이 크다고 해서 수익률이 좋은 것은 아니다. 반대의 경우도 마찬가지이다.

    변동성의 측정, 즉 척도도 여러 논란이 있다. 가령 공매도를 한 투자자를 제외한 투자자들은 하락장의 변동성을 좋아하진 않는다. 오히려 상승장의 변동성은 반가워 한다. 상승장의 변동성이 클 수록 좋아한다. 변동성으로 대변되는 위험을 기피하는 속성과는 맞지 않는다. 변동성의 척도 가운데 하나인 베타는 많은 연구 결과 베타는 미래의 변동성을 예측할 수 없다는 결론을 얻었지만 지금도 많이 쓰인다.

    1992년 유진 파마는 같은 시카고대학 교수인 케네스 프렌치와 함께 위험과 수익을 주제로 논문을 발표한다. 이 논문은 1963년부터 1990년까지 9,500개의 주식을 대상으로 주가순자산비율, 주가수익비율, 시가총액이 가장 낮은 주식들의 수익률이 장기적으로 가장 높았으며, 베타가 높은 주식과 낮은 주식을 비교해보면 수익률이 거의 비슷하다는 결론을 얻는다. 그들의 결론은 베타로 측정한 주식의 위험은 수익률을 예측할만한 지표가 아니라는 것이다.

    파마와 프렌치의 연구결과 주가순자산비율과 주가수익비율이 가장 낮은 주식의 수익률이 가장 높고, 대형주보다 소형주 비율이 높다. 주가의 수익률과 높은 상관관계를 보이는 것은 베타를 비롯한 위험지표가 아니다.

    "Beta as the sole variable in explaining returns on stocks is dead."


    ‘하나의 위험지표로 수익률을 예측할 수 없다’는 논문 이후 1년이 지나 파마는 위험을 계산할 때 베타외에 가치척도(high book to market)와 다른 기준(market capitalization, smaller caps are riskier) 을 고려해야 한다는 새 위험이론을 발표했다

    VBA 아니고 엑셀에서 람다함수 만들기 오피스/VBA/Office.JS

    파이썬 람다함수는 익명함수로서  변수에 저장할 수 있다. 아래의 예는 람다함수인데, 만들어진 함수는 변수 yAxb에 저장한다. 이 함수는 값을 x에 받아 2를 곱하고 다시 1을 더하여 돌려준다.

    >>> yAxb = lambda x : 2*x + 1
    >>> yAxb(4)
    9

    자바스크립트(ES6)에서는 일명 화살표함수라는 것이 있다. 파이썬의 람다함수처럼 이 역시 함수, 변수 yAxb에 저장한다.

    // ES6
    const yAxb = x => 2 * x + 1;
    console.log(yAxb(4)); // 9

    그런데, VBA도 할 수 없는 것을 엑셀이 비슷한 것을 만들 수 있다. 엑셀의 '이름'기능을 이용하는 것이다. 엑셀의 이름기능은 A1, B2, C4:E7 과 같은 의미없는 셀 주소 대신 의미있고 기억하기 좋은 이름을 부여주는 것이다.

    워크시트의 각 셀주소는 일종의 메모리 영역이다. 프로그래밍에서 변수를 선언하면 자동으로 메모리에 변수를 붙여 주는 것인데, 셀주소가 메모리 주소이고, '이름'은 변수를 지정한 것으로 비유할 수 있다.

    파이썬과 자바스크립트(ES6)에서 변수에 함수를 저장한 것 처럼 엑셀은 '이름'에 수식(함수포함)을 지정할 수 있다. 다음 그림을 보면

    'yAxb'라는 이름을 주고 '=Sheet1!$B2*2+1'라는 수식을 정의하고 있다. B2라는 셀이 위의 파이썬, 자바스크립트 코드의 변수 x에 해당한다. 그래서 셀안에 '=yAxb'를 입력하면 B열의 각 행 값을 읽어와서 계산을 하게 된다.


    ImportError = CentOS7+Django2.2+Python3.6 파이썬/쟁고

    PC에서만 돌려보던 쟁고프로젝트를 외부서버에 올리려고 하다보니, 프로그래밍이 아닌 쟁고 실행환경을 꾸리는 게 더 힘들다. /etc/httpd/conf/httpd.conf도 봐야하고, /etc/httpd/conf.d/vhost.conf 도 만들어야 하고, wsgi.py를 수정해야 한다. 시스템에 기본적으로 설치된 파이썬이 2 버전이고, 이에 맞춰 쟁고도 1.7.3(파이썬 2.7버전에게 가장 최선인 쟁고버전) 설치하였다. 그러나 아무래도 3 버전이 필요해 설치했고 쟁고도 2.2버전을 설치했다.

    우선 python manage.py runserver를 실행하면, 아래와 같은 에러메시지가 나온다.
    django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17)

    SQLite를 업그레이드 해야 하나 싶어 시도했지만, 결과는 삽질+삽질=실패
    문제는 쟁고2.2.가 SQLite 와 문제가 있다고 한다. 그래서 지우고 2.1로 다운그레이드하니, 개발서버실행은 잘된다.

    그리고 systemctl restart httpd.server를 하였다. 짜잔~ 서버 IP 입력해보니(도메인 따윈 필요없다. 남자라면 IP Address 정도는 외우..) 대충 다음과 같은 에러가 나온다.
    ImportError at /
    cannot import ...
    Request Method: GET
    Request URL:
    Django Version: 1.7.3
    Exception Type: ImportError
    ...
    Python Version: 2.7.6
    ...

    그러나 위의 에러 메시지를 보니 이 넘의 아파치가 뭔 넘의 의리가 넘치는 지 자꾸 2버전을 호출하는 것 같다. 그런데, 문제해결은 파이썬 2, 쟁고 1.7.3.을 지우는 게 아니었다(특히 파이썬2는 시스템에서 완장을 차고 뭔가 역할을 하고 있어, 지우면 안된다)
    이런 문제를 찾아보니 다음과 같은 답변이 있다.
    I believe that mod_wsgi is compiled against a specific version of python, so you need a py3.4 version of mod_wsgi.

    즉 버전에 맞는 mod_wsgi를 설치해야 한다는 것이다. 그러면 설치해야 하는 데, 설치할 mod_wsgi의 정확한 이름을 알아야 한다.
    다음은 yum search mod_wsgi 명령으로 검색한 결과이다.
    $ yum search mod_wsgi
    [...]
    koschei-frontend.noarch : Web frontend for koschei using mod_wsgi
    mod_wsgi.x86_64 : A WSGI interface for Python web applications in Apache
    python27-mod_wsgi.x86_64 : A WSGI interface for Python web applications in Apache
    python36-mod_wsgi.x86_64 : A WSGI interface for Python web applications in Apache

    그래~ python36-mod_wsgi 넘을 설치해야 하는 구나. 그래서 다음과 같이 설치하니
    yum install -q -y python36-mod_wsgi

    평범한 깡통 쟁고 프로젝트가 뜬다. 감격을 해야 하나 싶다.

    사람의 마음이 간사하다. 쟁고가 안풀리니까, 내심 '아파치 정지하고, 노드로 해봐야 하나...' 를 궁리하기도 하였다. 다음은 이런 저런 자료를 찾다가 본 자료인데,
    [자바스크립트와 파이썬의 대리인인 노드와 쟁고, 웹개발에 어느 게 나을 까?] 엄마가 좋아, 아빠가 좋아 급의 질문인듯하다.

    (스핀오프)날짜+시간에서 시간만 빼오기 오피스/VBA/Office.JS

    지난 번 MOD()함수 포스팅에서 아래와 같이 날짜+시간에서 시간만 빼오는 방법을 보여주었다.
    사실 엑셀은 날짜와 시간을 특별한 데이터 형식이 아닌 실수값으로 저장하고 있다. 정수부분은 1900년 1월 1일(기억이 정확하지 않는다)부터 세어온 일수이다. 그리고 소수점이하 숫자인 0.8989982639 는 오후 9시 34분을 가리키며, 이는 자정에서 시작하여 오후 9시 34분은 0.8989982639 , 약 89.89% 지난 것이다. 그래서 날짜와 시간이 같이 있는 데이터가 있다면 , MOD함수를 사용하여 시간부분을 뜯어낼 수 있다.

    그런데 방법은 이것만 있는 것은 아니다. 엑셀의 함수중 NOW()함수는 현재날짜와 시간을 돌려주는 함수이다. TODAY()는 현재 날짜만 돌려주는 함수이다. 두 함수의 뺄셈(=NOW()-TODAY())은 시간을 돌려준다. 위에서 설명한 바와 같이 엑셀은 날짜를 실수값으로 처리한다. 정수부분은 날짜, 소수점이하는 시간을 가리킨다. 따라서 뺄셈연산의 결과 소수점만 남긴다. 즉 시간만 남는 것이다.

    두 번째 운수좋은 날 - HP 10.1-inch Pavilion 10 Touchsmart Notebook PC 삽질의 추억

    지난 번 첫 운수 좋은 날에 이은 두 번째 운수좋은 날 시리즈이다.
    HP 10.1-inch Pavilion 10 Touchsmart Notebook PC (Sparking Black)
    - (AMD 1GHz, 2GB RAM, 500GB HDD, AMD A4-1200, Windows 8.1)

    동생이 사용하던 노트북이 있었는데, 앙증맞은 크기에 터치스크린을 가진 넘이었다. 그런데 이 넘은 애물단지였다. 메모리, CPU, 스토리지 등 맘에 두는 스펙은 아니었다. 게다가 터치스크린은 종종 오류를 일으켜서 귀신터치(건들지도 않았는데, 뒷목잡고 쓰러지는 자해공갈단, 양🐏아치 같은 터치)로 인해 화면을 계속 터치한 증상을 보인다.

    뭐 이거야 장치관리자에서 꺼버리면 되는 거니까 넘어가고... 암튼 동생의 노트북을 묻고 더블로 가서 어디서 구한 중고노트북(삼성 뭐 거시기인데, 10년전 모델이라 최대설치가능메모리 8기가인...)을 주었다.

    나는 동생의 노트북을 함 사용해볼까 싶어, 8기가 메모리를 추가하였다. 그래도 A4 CPU(CPU를 A4 재생용지로 만들었냐? 성능이 이따위냐?)와 HDD의 환장할만한 콜라보 덕에 성능은 그저 그렇다. 흠~ OS 설치는 귀찮지만 HDD버리고 SSD로 가자~ 문제는 OS 재설치이다. 윈도10이 최신이지만 이넘에겐 윈도8.1이 최상일 것 같았다. 사실 저 사양에서 8.1이 10보다 나을 것이다. 그래서 HP에 문의를 해보았다(윈도 8.1은 소매판매도 하지 않는 넘이다)

    (나) : '거~ 새 SSD에 윈도8.1 재설치하고 싶은 데, 어떻게 하나요?'

    그러나 어눌한 말투(고객센터가 중궈에 있는 듯)의 HP상담원 한 마디에 귀를 의심했다.

    (상담원): '그넘은 SSD 설치해봐야 소용없어. SSD 따위는 쌩깔거야!! 그러니 생긴대로 살어~'
    (나): '누나! 그거 사실이야? 인터페이스는 SATA인데...누나가 여기 온지 얼마 안되어서 이 바닥을 잘 모르는 것 같은데 말이야..'

    헐~ 이 누나 ~ 교육을 제대로 못받았네. 세상에 그런 하드웨어가 어딨어...
    HP가 SONY냐? 그런 이상한 짓을 하겠어(SONY 미안). 누나의 말을 개 무시하고 개복수술을 했다.
    드뎌 부팅~ 근데 이넘이

    (머신): '저장장치 없다 주인넘아~ 하드는 무너졌나? 쉐캬'

    (나):
    '왜 SSD를 사왔는데 먹지를 못하냐'

    하아~ 그 누나 말이 진짜구나. 누나 잘못했어~ 나의 오만을 용서해줘~ 내가 건방졌어



    생애 처음으로 본 VBA 버그 오피스/VBA/Office.JS

    시작은 어떤 분이 외부 엑셀파일을 불러와서 데이터를 복사하는 문제를 해결해주려는 오지랍 덕분이었다. 거의 코드를 만들고 테스트를 하는 데, 대화상자에서 선택한 파일명을 기록하였다. 그리고 다시 이 파일을 열려고 하니 파일이 없다는 식의 시비를 건다. 수 차례의 삽질 끝에 알아 낸 원인은 파일명에는 "[" 와 "]" 때문이다.
    1. Application.GetOpenFilename()함수는 "[" 와 "]"가 있는 파일명을 돌려준다.
    2. 그리고 Workbooks.Open() 역시 "[" 와 "]"가 있는 파일을 열어준다.
    3. 그러나 열어 둔 워크북 개체의 이름을 조회하니 "[" 와 "]"를 "(" 와 ")" 로 바꿔 버리는 것이다.
    4. 실제 엑셀에서 다른 이름으로 저장시 "[" 와 "]"는 이름으로 허용되지 않는다.
    5. 그러나 탐색기에서는 이름 변경시 "[" 와 "]"가 허용된다.
    말로 하니 복잡하니 아래의 코드를 실행하면, 열려는 파일명에는 "[" 와 "]"가 있고 잘 열린다. 그러나 wbk.Name은 "[" 와 "]"를 "(" 와 ")" 로 바꿔 보여준다.
    Sub demoBug()
    Dim strFileToOpen As String
    Dim wbk As Workbook

    strFileToOpen = "[BlackRock]_iShares_Global Product List June 30, 2015.xlsx"
    Set wbk = Workbooks.Open("C:\Kings\" & strFileToOpen)

    ' [BlackRock]_iShares_Global Product List June 30, 2015.xlsx 출력
    Debug.Print strFileToOpen

    '(BlackRock)_iShares_Global Product List June 30, 2015.xlsx 출력
    Debug.Print wbk.Name
    End Sub
    포스팅 분류를 '삽질의 추억'으로 분류해야 하나...그런데 다시 생각해보면 이건 버그는 아닌 듯, 엑셀개발팀과 윈도개발팀간에 손발이 안맞는 건가?

    리액트가 문제가 아니라 손가락이 문제야~ 자바스크립트

    실시간으로 시간을 출력하는 react-live-clock를 사용해보는 데, 간단한 코드 하나 해보는 데, 3~4번의 에러가 생긴다. 그중 하나 빼고 나머지는 ClockClcok 등으로 오타는 내는 데, 중복되는 알파벳을 하나 건너 반복하려는 무의식땜에 자주 이런다.
    import React,{Component} from 'react';
    import Clock from 'react-live-clock';

    class LiveClockTest extends Component{
    render(){
    return(
    <div>
    <Clock format={'YYYY-MM-DD HH:mm:ss'} ticking={true} timezone={'US/Pacific'}/>
    </div>
    )
    }
    }
    export default LiveClockTest;

    아싸 함수, MOD의 활용 오피스/VBA/Office.JS

    MOD함수는 나눗셈의 나머지를 돌려준다. 인싸인 VLOOKUP과 달리 사람들이 기억해주지 않는 아싸함수이다. 얘를 첨 보았을 때, 이해가 가지 않았다. 뭐 이런 함수를 뭐하러 만들었지...별 쓸데없어 보이는데.. 기껏 사용하는 것이 짝수행이나 홀수행마다 색칠을 하거나 합계를 구하는 수식에서 사용되는 정도.

    다음은 MOD함수의 기본적인 사용법이다. 8을 2로 나누면 나머지는 0이고 이 값을 돌려주는 것이 MOD함수이다. MOD함수에겐 '몫' 따위는 1조차 관심없다.

    MOD함수를 이용하면 소수점을 가진 실수에서 소수점 이하의 값을 구할 수 있다. 가령 43773.8989982639에서 소수점이하를 얻으려면 어떻게 할 까? 눈으로 보면 쉬운 건데, 막상 방법은... 간단하다. 이 숫자를 1로 나누는 것이다. 1로 나누면 소수점 앞의 값이 몫이고 그 이하는 나머지이다.
    = MOD(43773.8989982639, 1) = 0.8989982639

    그런데 위의 예에서 사용한 숫자 43773.8989982639 는 그냥 나온 것은 아니다. 이 숫자를 입력하고 셀 서식을 날짜로 바꾸면 '2019년 11월 04일 오후 9:34'이다. 사실 엑셀은 날짜와 시간을 특별한 데이터 형식이 아닌 실수값으로 저장하고 있다. 정수부분은 1900년 1월 1일(기억이 정확하지 않는다)부터 세어온 일수이다. 그리고 소수점이하 숫자인 0.8989982639 는 오후 9시 34분을 가리키며, 이는 자정에서 시작하여 오후 9시 34분은 0.8989982639 , 약 89.89% 지난 것이다. 그래서 날짜와 시간이 같이 있는 데이터가 있다면 , MOD함수를 사용하여 시간부분을 뜯어낼 수 있다.


    (엑셀도감) 컬럼 숨기기 단축 키 오피스/VBA/Office.JS

    [CTRL+0] 키는 현재 컬럼을 숨기기도 하지만 , 미리 여러 개의 컬럼을 선택(연속 또는 띄엄띄엄 선택)하고 사용해도 컬럼을 숨긴다.

    어느 운수 좋은 날 삽질의 추억

    어느 날 데스크탑(삼성 DM-C410)이 들어왔는 데, 십여년 전 많이 보던 디자인이다. 부팅해보니 당연히 HDD이다. OS는 윈도우7(32비트) 그리고 램(RAM)은 충격적인 2GB! 나의 구린 스마트폰도 4GB인데, 이게 현실인가?

    그래도 업그레이드를 하면 미운 오리새끼가 백조로 거듭나지 않을 까 싶어, 과감하게 8GB 메모리를 2개=16GB, 하드디스크도 삼성EVO250G SSD를 바꾸고 윈도10으로 업그레이드를 시도하였다. 도합 비용이 15만원 들었는데, 이럴 바엔 중고컴을 하나 사는 게 낫겠다는 현타가 온다. 그나저나 상콤한 윈도 10 설치후 시스템 정보를 보니 메모리가 8GB로 표시된다.

    헐~ 머리속을 확 지나가는 짐작은 '아하 이 넘의 스펙이 8GB이구나~'였다. 예전 같으면 삼성전자 홈페이지 가서 모델명 검색해가면 스펙을 확인했지만, 그냥 편하게 삼성전자 서비스로 문의를 해보았다. 엔지니어도 2011년이라니까 그 당시에 최고 한도가 8GB일 거란다. 그래도 확인해보라고 했더니 역시 8GB가 한계였다.

    그러면 뱅크를 모두 채울 필요는 없으니 하나는 빼두자. 그래서 다시 뱅크에서 메모리 하나 빼고 부팅. 그러고 다시 확인해보니 4GB로 표시된다. '아하~ 뱅크별 메모리 한계가 4GB 이구나' 결국 4GB씩 도합 8GB를 인질로 잡혀있는 셈이다. 16GB를 사두고 8GB만 사용하자니 현진건 단편 '운수 좋은 날'이 떠오른다.
    배경은 1920년대의 서울이다. 어느 비오는 날, 인력거꾼 김첨지는 그날따라 유독 가지 말라고 말리는 병든 아내를 두고 돈을 벌러 나온다. 그런데 그날따라 유독 손님이 많아서 김첨지는 많은 돈을 벌었다. 하지만 집에 가까이 갈수록 어떤 알 수 없는 불안감이 느껴져 불길해 하던 중, 마침 친한 친구 치삼이를 만나 그와 술을 마시며 시간을 보낸다. 술에 취한 상태에서도 아내가 그리도 먹고 싶다던 설렁탕을 사서 집에 돌아갔는데....설마설마하던 불안감을 계속해서 느끼던 김첨지는 결국 아내가 죽은 것을 확인하고는 그 시신을 붙들고 절규하며 "왜 설렁탕을 사왔는데 먹지를 못하냐"고 울부짖으며 절망하는 것으로 끝이 난다.(출처: 운수 좋은 날)


    '왜 16GB를 사왔는데 먹지를 못하냐' 나의 운수 좋은 날은 이렇게 끝난다.


    똥휴지로 설명하는 프로그래밍 파이썬/쟁고


    1 2 3 4 5 6 7 8 9 10 다음