Terra Brandford.
Material: 4B&H pencil, Sketchbook
저작자 표시
신고

늦가을 밤의 추위는 매서웠다. 그날따라 바닷가의 바람이 거세게 불었다. 새하얀 거품을 일으키며 부산히 몰려오는 작은 파도의 무리는 공허함을 깨는 메아리처럼 아우성쳤다. 싸늘하고 적막하기까지 한 이곳 해변을 찾는 이는 아무도 없을 터, 하지만 갈 곳을 잃은 한 중년의 남자가 마침내 이곳 해변의 모래사장에 도달했다. 모래사장 너머 새롭게 단장한 커다란 여관은 야광을 비추고 있었다. 그는 그 야광을 등지고 공허한 모래밭 위에 서서 일렁이는 암흑 바다를 마주하였다. 파도는 마치 깊은 심연 속에서 아지랑이처럼 피어났고 그에게 점진적으로 다가왔다. 그는 한동안 춤추는 파도의 유희를 말없이 바라보았다. 파도의 파장은 춤추듯 출렁이며 그를 향해 몰려왔지만, 그의 발끝에 도달하기 전에는 모두 소진되고 없어져 버렸다. 그는 그것마저 아쉬운 표정을 보였다.


여기에 오기 직전 마셨던 벌꿀주의 술기운에 그의 몸은 뜨겁게 달아올랐다. 바닷바람은 뜨거운 그의 몸을 식혀주었다. 그는 기분이 좋았다. 하고 싶은 말이 많았지만, 그의 주변엔 아무도 없었다. 그는 검은 바다 위에 눈에 띄게 대조적인 파도의 하얀 거품을 바라보며 싸늘한 바닷바람을 만끽하였다.


태어나자마자 온 힘을 다해 그를 향해 달려온 후 아무것도 남기지 않고 사라져 버리는 바다 위의 무수히 많은 파도 조각들. 뜨거운 불은 재라도 남기지만, 저 파도는 무엇을 위해 열정적으로 한순간을 살고 사라져 버리는 걸까. 만일 그들에게도 삶이 존재한다면 일 분도 채 안 되는, 하루살이보다 더 짧은 조각 같은 인생일지라. 만약 파도 같은 생이 그에게 주어진다면 과연 그는 그 순간 무얼 하고 떠날 것인가? 조금은 바보 같은 가정인 줄 알면서도 그는 눈앞에 보이는 파도와 같은 생을 사는 생각에 잠겨 있었다. 하지만, 생각할수록 그에겐 그 어떤 것도 의미가 없을 것만 같았다. 인연? 소유? 명예? 깨달음? 그토록 짧은 생에선 그 어떤 것도 이루지 못할 터. 세상에 의미 없는 흔적을 남길 바엔 그냥 저 파도처럼 아무런 흔적없이 사라져 버리는 것이 가장 나을 것 같았다. 


그는 성자가 되기에는 인간에게 주어진 시간은 저 파도처럼 너무 짧다고 느꼈다. 인간은 결코 깨달음을 얻을 수 없을 지라. 얻는다 할지언정 미완으로서 죽음을 맞이할 것이다. 하지만, 세상의 성자들이 그러한 미완의 깨달음을 마치 진리인 양 세상에 전파하고 다닌다는 사실에 그는 치가 떨렸다. 그는 결국 인간이 한평생 살지언정 깨달음을 얻진 못한다고 생각했다. 도리어 저 파도처럼 아무것도 남기지 않고 그냥 아무 흔적이 사라지는 것이 어쩌면 성자로서 최선이라는 확신이 생겼다. 간혹, 세속을 피해 아무도 없는 곳으로 떠났다는 어느 성자들의 이야기가 이제는 존경스럽기까지 했다. 그들처럼 되지 못한 자신이 원망스러웠다. 그는 과거에 저지른 자신의 어리석은 잘못을 떨쳐내고 싶었다. 그는 무의식적으로 자신을 향해 몰려오는 파도를 향해 한 걸음씩 다가가기 시작했다. 그의 발이 서서히 바닷물에 잠겼다. 뼛속까지 잠식해오는 냉온의 아찔함을 견디며 그는 그대로 직진했다. 그는 자신의 괴로움과 고통을 눈앞의 검은 바닷속에 모두 내던져야만 했다. 그는 끊임없이 몰려오는 파도에 저항하며 조금씩 전진하였고 어느덧 그의 몸은 검은 바다의 심연 속으로 완전히 잠기고 말았다.


에드문드는 한때 미덕이 확고한 사람이었다. 그가 성인이 되던 날, 아버지의 부름을 받고 숭고한 이들 앞에서 정의로운 삶을 살기로 맹세했다. 그들 앞에 무릎을 꿇고 기도를 드렸을 때 그의 영혼은 그들로부터 영성을 부여받았다. 신성한 영혼은 그의 정신을 맑게 했다. 그가 이전에 바라보았던 세상은 모두 사라졌으며 모든 만물이 새로운 가치와 희망으로 반짝이는 것을 확인할 수 있었다. 그는 그 빛을 더욱 밝게 비추리라 결의에 가득 찬 뜨거운 가슴을 가지고 세상을 향해 길을 떠났다. 


영성을 부여받은 그는 성자의 길을 가야만 했다. 영성을 위한 기본 자질은 동정, 희생, 정직, 그리고 숭고였다. 그는 선대 위인들의 숭고한 정신을 따르기 위한 미덕을 한순간도 잊지 않으려고 애썼다. 그는 길을 떠나면서 어렵고 힘든 이들을 동정하고 그들에게 도움을 주기 위해 자기 자신을 희생하는 것도 거리낌 없이 행하였다. 그는 수행을 지속하며 깨달음을 얻었으며 이를 전파하였다. 선행도 지속하였다. 그러자, 어느 순간부터 사람들은 그를 위대한 선지자라고 칭하였다. 그는 십 년을 쉬지 않고 떠돌면서 세상에 자신의 도움의 손길이 필요한 곳은 어디든 찾아갔다.


그러던 그는 수도 중에 우연히 한 작은 시골 마을에 다다랐다. 그 마을은 서쪽 깊은 산골짜기에 있었는데 외부에는 잘 알려지지 않은 곳이었다. 마을은 겨우 스무 가구가 살 만큼 매우 작았다. 이곳에 막 도달했을 때 에드문드는 떠들썩한 거리의 광경을 목격하였다. 주홍 머리칼의 한 젊은 여인이 거리에서 주민들로 보이는 이들에게 몰매질을 당하고 있었던 것이다. 몰매는 한동안 지속되었다. 사람들은 그 여인을 증오하고 경멸하였다. 이유인즉슨, 그녀가 외도했다는 것이었다. 그것은 그 마을에선 도저히 용납할 수 없는 처사였다. 사람들은 그녀의 인성이 그릇됐다고 하면서 그녀가 돈을 벌러 간 남편에게 배신과 돌이킬 수 없는 상처를 남겼다고 하며 그녀를 마을에서 쫓아내려고 하였다. 여인은 옷이 여기저기 찢기고 온몸이 피멍이 든 채 거리에 쓰러져 흐느껴 울고 있었다. 남자는 그 여인을 그냥 지나칠 수가 없었다. 사람들을 제치고 나선 그는 자신이 에드문드 수도승이라고 하였다. 사람들은 에드문드라는 이름에 모두 놀라 했다. 그 외딴 마을마저 이미 이 존경스러운 선지자의 이름이 알려져 있었다. 그는 그녀를 업고 그 마을을 벗어났다. 


마을 인근에 버려진 작은 오두막에 그녀를 데려온 남자는 그녀를 잘 보살펴 주었다. 그녀에게 물과 음식을 주었고 흙과 함께 피부에 말라 굳어버린 피를 닦아주었다. 찢어진 그녀의 피부에 약을 바른 후, 약초와 헝겊을 덧대 상처가 아물도록 도와주었다. 그녀는 정신적, 육체적으로 큰 상처를 받은 것처럼 보였고 종일 아무 말도 없었다. 남자는 그녀에게 어떠한 질문도 하지 않았고 그저 옆에서 그녀를 지켜보았다.


자정을 지날 무렵, 어느 정도 안정을 되찾은 그녀가 살며시 입을 열었다. 선잠에 빠져있었던 에드문드는 바로 잠에서 깨어나 그녀의 말에 귀를 기울일 수 있었다.


"남편이 돈을 벌기 위해 북쪽의 금광으로 떠난 후, 다른 남자와 눈이 맞았어요. 안데라스라는 청년이었죠. 황금빛 머리칼을 가진 그는 햇살처럼 따사로운 남자였어요. 특히 눈이 예뻤고 미소는 달콤했죠. 어느 화창한 봄날의 토요일 오후, 마을의 호수에서 그와 처음 마주쳤어요. 그는 호숫가에 앉아서 따스한 봄날의 정취를 홀로 만끽하고 있었죠. 저는 한눈에 그가 이방인이라는 것을 알 수 있었어요. 저희 마을은 매우 작기 때문에 마을 사람들은 서로를 잘 알거든요. 그는 떠돌이였는데 우연히 이곳을 찾아왔다고 했어요. 마치 선생님처럼요. 저를 마주한 그는 저에게 마을에 대해 이것저것 물어보았죠. 때마침 홀로 산책에 나섰던 저는 말동무가 생겨서 좋았어요. 그와 함께 호숫가를 거닐며 이야기를 나누었죠. 우리 마을에 대해서 특별히 해줄 이야기는 없었지만, 오히려 그는 저에게 다양한 경험을 공유해 주었어요. 그가 다년간 여행을 다니면서 겪었던 이야기는 저에겐 매우 놀랍고 황홀하기만 했죠. 제가 가보지 못했던 지역의 문화와 음식 그리고 유적지에 대한 이야기는 제가 책에서 읽었던 것과는 아주 달랐어요. 그의 이야기는 훨씬 더 생동감이 넘쳤고 마치 제가 그곳에 간 것과 같은 경험을 선사해 주었죠. 그의 말솜씨는 재능있었어요. 같이 이야기를 나누는 동안 시간가는 줄 몰랐죠."


아늑하게 타오르는 촛불과 그녀의 붉은 머리칼은 이질감이 없을 정도로 자연스럽게 동화되어 있었다. 그녀의 초췌한 얼굴마저 수줍은 듯 붉게 물들어 있었다.


"그는 홀로 이곳에 왔기 때문에 결국 저녁에 같이 식사를 하게 됐어요. 그는 저와 더 이야기하면서 시간을 보내고 싶다고 했죠. 아뇨, 사실 제가 먼저 그를 집으로 초대했어요. 사실, 그러면 안 된다는 것을 알고 있었어요. 하지만, 제 몸은 어쩔 수가 없었어요. 저는 이미 알게 되었죠. 제가 이미 그의 매력에 빠져버렸다는 것을. 모르겠어요. 그냥 그는 너무 매력적이었어요. 그의 외모는 물론, 그에게서 풍기는 표현하기 어려운 분위기를 뭐라 표현해야 할지. 선생님께서는 제가 더러운 여자라고 생각하고 있으시겠죠. 하지만, 본능을 제어하는 것이 왜 그렇게 어려웠을까요? 사실 그와 더 같이 있고 싶었고 어두운 밤 그를 저의 집으로 초대하는 것은 제 남편을 기만한 행동이라는 것도 알고 있었죠. 남편은 저 먼 곳에서 하루하루 힘겹게 금광을 캐고 있었겠죠. 하지만, 제가 남편을 떠나겠다고 다짐한 것은 아니었어요. 그저 단 하루, 마치 족쇄에 꽁꽁 묶여버린 듯한 저의 말라버린 영혼에 자유를 주고 싶었어요. 가여운 제 본능에 자유를 허락하고 싶었어요. 제 인생에서 잊어버린 뜨거움을 다시 느껴보고 싶었어요. 차라리 그날 죽어버릴지라도 후회 안 할 만큼 저는 그 갈망에 허덕이었죠. 결국, 그날 밤 그에게 제 몸을 허락하였고 우리는 잊지 못할 한순간을 같이 보냈어요."


남자는 그녀의 이야기에 귀 기울였고 그녀는 잠깐 말이 없었다. 그녀는 남자를 물끄러미 쳐다보다가 다시 말을 이어갔다.


"다음날 안데라스는 마을을 떠났어요. 멀리 아무도 모르는 곳으로 말이죠. 정말 그는 바람 같은 인생을 사는 자유로운 영혼의 소유자 같지 않나요? 정착만 해 온 저에게는 조금은 부러울 따름입니다. 이제 저희는 다시는 만날 일은 없겠죠. 그럴지언정, 저는 그를 비난하지 않고 오히려 감사해요. 남편에게는 비밀이지만 저는 제 인생을 살다가 간혹 그날을 회상할지 모르죠. 그와 단 하루, 뜨거운 사랑을 속삭였던 그 순간 말이죠. 일 년 전, 집을 떠난 제 남편을 하루하루 기다리면서 지쳐 말라버린 저의 정신과 육체에 그는 맑은 샘물과도 같았어요. 제 얼굴을 마주하던 그의 부드러운 미소, 어린아이처럼 달콤한 입술. 그리고 따스한 체온. 하지만, 그것보다는 그의 뜨거운 심장 고동 소리가 메아리처럼 퍼져 저의 가슴에 도달하는 순간, 저는 그 요동에 심장이 터질 듯 미쳐버리는 줄 알았죠. 남편을 처음 만난 이래로 잊고 있었던 사랑의 달콤함을 느꼈어요. 정말 오랜만이었죠."


"정말 아이러니해요. 인간이라는 존재는 육체적 사랑을 갈망하면서도 우리는 그것을 구속해야만 하죠. 그리고 그날 하루, 우리는 우리 둘의 육체와 영혼이 불꽃처럼 타올랐고 마치 전소하듯 사라져 버렸죠. 그리곤 타고 날아가 버린 연기처럼 아무것도 남지 않은 채, 그렇게 우리 둘의 관계는 끝이 났어요."


그녀의 말이 끝나기가 무섭게 남자가 말했다.


"여인, 사랑은 고귀하고 육체적 욕망은 신이 주신 자연의 섭리지. 그것을 부정할 순 없다오. 여인의 그 욕망을 그 누구도 비난할 순 없지. 세상 모든 사람도 마찬가지오. 그들은 겉으로 표현하지 않지만, 때로는 내면엔 그러한 욕망을 주최하지 못하고 괴로워한다오. 나라고 다를 바 없지 않소. 하지만, 잘 들어보시오. 이 일이 당신과 그 안데라스라는 청년 둘 사이의 관계로 끝날 수 있다고 생각하시오? 여인의 남편이 이 사실을 알게 된다면 그가 받을 상처는 아무것도 아니란 말이오? 신은 인간에게 육체적 욕망을 부여하기 전에 그것을 조절할 수 있는 정신과 감정의 능력을 주었소. 여인처럼 모두가 그걸 제대로 조절하지 못하면 이 세상은 결코 용납될 수 있는 사회가 되고 말 것이오. 보시오, 이 일로 인해 결국 여인은 마을에서 쫓겨났고 큰 상처를 받지 않았소?"


"선생님 말씀이 맞아요. 그럴지언정, 저는 인생에서 단 하루, 잊지 못할만한 경험을 했다는 사실은 변함이 없어요. 제게 그렇게 후회할만한 일인지 곰곰이 생각해 보았어요. 어두운 방에서 제 옆에 있지 않은 남편을 기다리며 혼자 보내는 그 시련은 그 어떤 고통과도 바꿀 수 없었어요. 제 꽃은 시들었고 남편을 미워하기 시작하였죠. 제 마음의 상처는 커져만 갔어요. 오히려 지조를 지키는 동안 제 영혼과 육체는 병들어 가는 듯했어요. 지독한 고독의 시련은 제 영혼의 공간에서 독처럼 검게 퍼져나갔죠. 그 날, 안데라스를 호숫가에서 외면했다면, 저는 분명 그토록 달콤한 경험을 제 인생에서 해보지 못했을 거예요. 하지만, 그러지 않았기 때문에 저는 잊지 못할 보석 같은 경험을 하였고 그 날 이후 저는 새로운 하루를 맞이한 듯했어요. 그날을 후회하지 않아요. 제 인생의 한 공간에는 분명 안드레스가 존재할 테니깐."


여인은 깊은 한숨을 내뱉은 후 계속 말을 이어나갔다.


"제 남편은 이 사실을 모르고 있겠죠. 그리고 그렇게 영원히 모를지도 몰라요. 정말 그랬으면 좋겠어요. 저 때문에 남편이 힘들고 괴로워하는 것을 저 역시 원치 않기 때문이죠. 하지만 이 세상이 제가 희망한 대로 흘러가는 건 아니겠죠? 물론 그러기 전에 저는 남편에게 편지를 보낸 후, 이 마을에서 멀리 벗어나 남편과 새롭게 시작할 거예요. 마을 사람들의 손이 닿지 않는 곳으로 말이죠. 남편이 이 사실을 모르게 하고 싶어요."


여인이 남편을 속이고 싶어 한다는 말은 에드문드에게 가시거리처럼 들렸다. 에드문드에겐 거짓이란 있을 수 없었다. 그는 성자의 길을 걷는 동안, 무수히 많은 일을 겪었다. 거짓과 속임수는 결국 세상을 어지럽힌다는 진리는 틀리지 않았었다. 진실은 선하다는 사실을 그는 절대적으로 신뢰하였다. 에드문드는 고개를 가로저었다. 


"선생님, 저는 제가 무얼 저질렀는지 잘 알고 있어요. 지조가 없다고 말씀하실 수도 있겠죠. 하지만, 그렇게라도 하고 싶었어요. 가질 수 없다는 것을 알면서도. 그 일을 피하지 못했던 저 자신을 원망하면서도 저는 한편으로는 기뻐요. 기쁘다고 표현한다면, 분명 선생님은 저를 이해하지 못하실지도 몰라요. 하지만..."


"여인, 죄를 지은 자가 처벌을 받아 마땅한 것처럼, 불의에 대한 정의가 없으면 사회는 파괴되고 말 것이오. 마을 사람들이 여인을 손가락질하고 마을에서 쫓아낸 것도 나는 이해할 수 있을 듯하오. 여인이 기쁘다는 사실도 진실이라고 믿소. 하지만 여인의 그 거짓된 욕망이 결국엔 여인을 막다른 골목으로 몰고 갈 것이오. 남을 배신하고 거짓으로 그것을 감추려고 하는 자세는 옳지 않소. 진실은 선하다는 진리를 잊지 마시오. 진실을 구하되 용서로서 상처를 치유해야만 하오. 여인은 고해하고 죄를 뉘우친 후, 남편과 마을 사람들에게 용서를 구해야 하오. 그것이 사람으로서 할 도리라오."


여인은 고개를 끄덕이다가, 고개를 바닥에 떨구었다. 그녀의 얼굴에서 눈물이 바닥으로 떨어졌다.


"선생님마저 그렇게 말씀하신다면, 그렇게 하겠어요. 마을 사람들에게 무릎을 꿇고 용서를 빌겠어요. 남편이 돌아오는 날, 그에게 진실을 밝히고 용서를 구하겠어요."


다음날 이른 새벽, 여인은 성당을 찾아가 고해를 하고 마을 사람들에게 용서를 구하기 위해 마을로 향하였고 에드문드는 오랜 여정으로 인한 피로로 그녀가 오두막에서 떠났는지도 모른 채 깊은 잠에 빠져있었다.


해는 이미 중천에 떠 있었다. 에드문드는 깊은 잠에서 깨어났다. 방 한구석에 잠을 청했던 여인은 없었다. 그는 그녀가 이미 마을로 향했을 것이라고 짐작했다. 간단한 채비를 마친 그 역시 마을로 천천히 발걸음을 옮겼다.


마을의 성당을 향해 발걸음을 옮기던 에드문드는 또 다른 부산한 광경을 목격하게 되었다. 사람들이 모여있었는데, 이번에는 뭔가 더 큰 일이 벌어진 것이 틀림없다고 에드문드는 직감했다. 그는 성당 앞에 버려진 듯 놓여있는 피투성이가 된 싸늘한 여인의 시신을 발견하였다. 어젯밤 그와 이야기를 나누던 바로 그 여인이었다.


"아..."


그녀를 못마땅하던 주민들이 결국 홧김의 동조로 그녀를 죽음으로 몰고 간 것이다. 에드문드는 전신에 힘이 빠지듯 휘청거렸다. 마치 이 일의 경위가 자신의 책임인 양, 보이지 않는 육중한 무게가 자신의 몸을 짓누르는 듯했다. 거기엔 특정한 살인자도 없었다. 하지만 곧, 그는 이 일의 결말은 이미 예정되어 있었던 것이라고 뒤늦게 깨달았다. 그녀는 죄를 지었으며, 그에 대해 대가를 받은 것이다. 그녀는 경솔했으며 그녀의 이기적인 마음이 이 일을 좌초한 것이었다. 이 결말은 그녀의 잘못에 대한 처벌이었다. 안쓰러웠지만, 이제 돌이킬 수도 없었다.


에드문드는 그녀의 시신을 그녀가 안드레스를 만났다던 그 호수가 인근에 묻어주었다. 에드문드는 그 마을을 황급히 떠나고 싶었다. 하지만 그것은 성자로서 해야 할 도리가 아니었다. 그는 마을 사람들로부터 새로운 가치를 발견하였고 그의 영성을 사람들에게 실천해야만 했다. 이후, 그는 마을 인근의 버려진 그 오두막을 손질하고 그곳에 머물면서 주민들에게 삶의 지혜와 깨달음을 전파하는데 몰두하는 나날을 보냈다.


그가 그 마을에 머문 지 반년이라는 시간이 흘렀다. 죽은 여인의 묘지에는 어느덧 풀이 무성했고 호숫가의 버드나무 이파리가 산들거리는 어느 봄날인 무렵, 어느 한 건장한 남자가 마을에 다다랐다. 그는 죽은 여인의 남편이었다. 그 남자는 그의 아내가 죽었는지도 모른 채, 한때 아름답고 사랑스러웠던 자신 아내의 모습만을 머릿속에 떠올리며 한가득 보따리를 매고 즐거운 귀향길을 걸어왔을 것이다. 그가 마을에 도착했을 땐, 마을 사람들은 그를 외면했다. 그의 아내가 자신들에게 죽임을 당했다고 차마 말할 수가 없었다. 그는 영문도 모른 채 한동안 버려져 있었던 자신의 집을 찾아갔으며 곧 그녀가 이 마을에 더는 존재하지 않는다는 사실을 알 수 있었다. 한동안 아내로부터 편지가 오지 않았다는 사실도 이해가 되었고 그는 허망하기 그지없었다.


한 편, 농사일하고 있었던 에드문드는 죽은 그녀의 남편 복귀 소식을 듣고 곧장 그녀의 집으로 향했다. 에드문드가 집에 도달했을 땐, 남자는 허망한 표정을 지으며 슬픔에 잠겨 있었다. 에드문드는 그에게 그녀의 죽음을 알려주었다. 그는 마치 짐작이라도 하고 있었다는 듯이, 그녀의 죽음은 도대체 무엇을 위한 것이었는지 에드문드에게 바로 되물었다. 에드문드는 그를 그의 아내의 무덤으로 데리고 갔다. 그리고 그녀에게 있었던 일을 그에게 상세히 설명해 주었다. 그는 망연자실한 채 그녀의 무덤 앞에 무릎을 꿇고 한동안 아무 말도 하지 못했다. 


해가 질 무렵, 호숫가에는 핏빛 석양이 물들어 있었다. 에드문드와 남자는 여전히 여인의 무덤 앞에 서 있었다. 남자는 혼잣말을 하듯 성자에게 말을 했다. 


"얼마나 그녀가 외로웠을까. 그녀가 그날 어느 젊은 청년과 눈이 맞았다고 했지만, 그녀는 그를 따라 이 마을을 떠나지 않았단 말이오. 그녀는 계속 나를 기다렸을 것이오. 그녀가 그날 외도를 했건 말건, 난 그러한 그녀의 모습조차 사랑할 수 있는 그녀의 남편이오. 그녀를 탓할 것은 아무것도 없었소. 애초에 그녀를 두고 간 건 나였소. 되레 그녀를 지켜주지 못한 내가 죄인이오."


두 눈에 눈물이 뚝뚝 떨어지는 그 건장한 남자의 두 눈동자에는 괴로움으로 가득 찼다. 에드문드는 깊은 후회와 절망에 빠진 그를 가만히 바라볼 수밖에 없었다.


그 날 저녁 자정을 넘은 시각, 마을은 고요히 잠들어 있었다. 여인들의 빨래터이자 작은 야외 회의장이 있는 마을의 중심지의 우물가엔 한 남자의 무거운 노랫소리가 구슬프게 울려 퍼졌다.



그녀가 있는 곳까진 그리 멀지가 않네


눈을 감은 당신은 먼저 나를 떠나갔고


어두운 그림자 후회로 흐느끼네


당신은 멀리서 나를 기다리네


나는 신께 맹세하오


지금 나 어둠과 함께 걸어갈 테니


그녀를 볼 수 있게 해달라고


촛불이 불타듯 내 마음도 간절하니


이제 그곳에서 너무 슬퍼하지 마시오


내 몸은 가벼우니 


한숨을 쉰 후, 지금 당장 발걸음을 옮긴다



곧이어 그 작은 마을에는 뜨거운 불길이 치솟기 시작했다. 남자는 마을을 뛰어다니며 미리 준비한 기름과 불통을 불이 탈 만한 곳 여기저기에 마구 던졌다. 헛간의 볏짚에 던졌으며 지붕 위로 던졌다. 집이 겨우 스무 채 되는 이 작은 마을을 지워버리는 일은, 미쳐 실성한 한 남자에게 있어서 결코 어려운 일이 아니었다. 곧이어 마을의 모든 집은 불길에 휩싸였으며 주민들의 비명, 통곡 소리, 울부짖는 소리가 여기저기서 울려 퍼지기 시작했다.


한 편, 마을의 밖 오두막집에서 잠에서 뒤척이던 에드문드는 멀리 마을 위 붉게 번뜩이는 저녁 밤하늘에 결국 잠에서 깨어났다. 그는 오두막에서 나와 마을을 향해 바라보았다. 삼키듯 타오르던 마을의 화염은 마치 악마 같은 연기로 서서히 뒤덮이고 있었다. 그는 불타는 그 마을의 광경에 눈을 뗄 수가 없었다. 그는 충격에 휩싸였으며 그의 심장은 두려움에 요동을 쳤다. 그는 눈앞의 그 공포를 받아들이기 너무 힘들었다. 불타는 마을에는 죽은 여인의 환영과 절망에 빠진 남편의 환영이 같이 일렁거렸다. 남자는 화염의 도시를 등지고 어두운 산자락 속으로 엉거주춤하며 도망치기 시작했다. 그 무엇도 생각할 수 없었다. 하지만 화염의 불길로부터 도망치는 동안, 그는 그동안 그가 걸어온 성자의 인생이야말로 경솔했고 거짓이었다는 사실을 알아차릴 수 있었다. 진실이 선하다는 그의 신념은 산산이 조각나며 완전히 무너져버렸다. 그 진실은 결국 파멸을 몰고 왔다. 그곳엔 모두를 위한 세상은 결코 없었다. 그의 눈앞엔 어떠한 선도 없었으며 그곳은 증오와 희생만이 존재했다. 그의 신념은 한 작은 마을에 파멸만 몰고 왔을 뿐이었다. 그를 믿던 사람들은 그렇게 화염의 불길 속으로 모두 사라져버렸다. 현실을 받아들이기 어려웠던 에드문드는 혼돈의 나락 속에서 살아남기 위해 발버둥 치듯 그렇게 어둠 속으로 사라졌다.



저작자 표시
신고


The winter soldier.
Material: 4B&H pencil, Sketchbook
저작자 표시
신고

These days, source code is more liked to opened to others than before. Many companies runs tremendous open-source projects, developers are more interested in the open-source projects for their careers.

In this topic, Hermet Park likes to share his open-source activity experience with attendees. He will not only describe why we are interested in the open-source projects but also talk about open-source activities describing his experiences.
저작자 표시
신고

The GUI is the the most important part of modern applications. It’s getting difficult to implement modern UX design for developers because it requires more intuitive and better intelligent user interactions. In the meantime, simple and unique design is a good point for application identity.

In this session, the speaker will first share the core concepts behind Tizen native programming, then will present how to create custom UI designs for Tizen applications, using elegant GUI tools.

Finally, he will demonstrate how to develop and custom design a fancy-looking application.

During this session, the focus will not be on Tizen specifically but on modern GUI application development with EFL, which is the core UI Toolkit for Tizen.


저작자 표시
신고

최근 사용자 경험은 소프트웨어에 있어서 매우 중요하다. 기능적으로 유사한 소프트웨어가 많기 때문에 더 세련된 디자인과 사용하기 편한, 즉 사용자 경험이 보다 뛰어난 소프트웨어가 사용자에게 매력을 더 어필할 수 있다. 물론, 빠르고 부드러운 비주얼 인터렉션은 당연지사이다. 앱 개발에 있어서 UI는 결코 사소한 요소에 해당되지 않는다. 앱 개발자는 보다 쉽고 빠르게 앱을 구현하기 위해 뛰어난 UI 프레임워크를 선호한다.

UI 프레임워크는 앱 개발자가 쉽고 빠르게 앱 화면에 UI를 배치하고 사용자와 앱 간의 상호작용을 수행할 수 있도록 도와준다. UI 프레임워크는 고성능의 화려한 비주얼 효과를 제공하기 위한 뛰어난 그래픽스 처리 엔진은 물론, 사용자 앱의 주 로직과 UI 처리 로직 간의 자연스러운 통합을 위한 메인루프(main loop)와 같은 핵심 로직도 제공한다. 게다가, 보다 쉬운 앱 개발을 위해 프로그래밍 인터페이스는 더욱 더 정교하게 설계되어 제공된다.

UI 프레임워크를 이해하는 가장 단순한 방법은 직접 앱 개발자가 되어서 필요한 기능을 사용해 보는 것이다. 이번 장에서는 앱 개발 관점에서 직접 앱을 구현하면서 UI 프레임워크의 기본 기능에 대해 짚어보고 한편으로는 프레임워크 개발 관점에서 어떻게 그러한 기능을 제공할 수 있는지 알아보고자 한다. 만약 여러분이 앱을 개발해 본 적이 없다면, 어쩌면 이번 장은 여러분에게 매우 적합한 장이 될 것이라고 생각한다.


1. 이번 장 목표

이번 장을 통해 다음 사항을 학습해 보자.

  • UI의 기본 원소와 UI 컨트롤에 대해 이해한다.
  • 앱의 UI를 생성하는 기본 메커니즘을 살펴본다.
  • 버튼과 이를 다루는 이벤트 사용 메커니즘을 배운다.
  • 앱의 기본 구조 및 라이프사이클를 이해한다.
  • UI 엔진의 개념과 메인루프에 대해서 살펴본다.


  • 2. UI의 기본 원소

    본격적인 시작에 앞서 우리는 UI의 기본 원소를 먼저 살펴볼 것이다. UI의 기본 원소는 앱 UI를 구성하는데 있어서 가장 기본이 되는 원소에 해당한다. 다음과 같은 앱의 UI를 구성하기 위해서 우리는 어떤 리소스가 필요할까?

    그림 1: 앱 화면 예

    그림 1은 우리가 잘 알고 있는 크롬(Chrome) 브라우저의 구글 페이지 화면이다. 크롬 역시 하나의 UI 앱으로 간주할 수 있는데 얼핏 보기에 UI가 복잡해 보이지만 사실 UI를 조목조목 뜯어보면 결국에는 이미지와 텍스트 두 요소로 구성되어 있음을 확인할 수 있다.


    그림 2: 앱 화면을 구성하는 기본 원소

    앱의 화면을 구성하는 기본 원소는 화면을 화려하게 장식해 주는 이미지와 문맥 정보를 전달하는 텍스트 두 가지로 축약해 볼 수 있다. 다소 기능이 원시적일지라도, 사실 이 두 기능만 존재한다면 어떠한 종류의 앱 화면도 정확히 구현할 수 있다. 이를 증명하기 위해 코드 1은 그림 1 중에서 검색 상자(Search Box)를 어떻게 구현할 수 있는지를 보여준다.

    그림 3: 구글의 검색 상자

    //검색 상자
    UIImage searchBox = new UIImage();             //이미지 생성
    searchBox.open(“./../SearchBox.png”);          //이미지 리소스
    searchBox.move(80, 300);                       //이미지 위치
    searchBox.resize(350, 50);                     //이미지 크기
    searchBox.show();                              //이미지 출력하기    
    
    //검색 상자 가이드 텍스트
    UIText guideText = new UIText();               //텍스트 생성
    guideText.text(“Search Google or type URL”);   //텍스트 설정
    guideText.color(“lightgray”);                  //텍스트 색상
    guideText.move(90, 310);                       //텍스트 위치
    guideText.resize(130, 40);                     //텍스트 크기
    guideText.show();                              //텍스트 출력하기
    
    //검색 상자 음성 아이콘
    UIImage banner = new UIImage();                //이미지 생성
    banner.open(“./../VoiceRecognition.png”);      //이미지 리소스
    banner.move(400, 310);                         //이미지 위치
    banner.resize(20, 25);                         //이미지 크기
    banner.show();                                 //이미지 출력하기
    

    코드 1: 앱 화면 구성 예

    코드 1처럼 이미지와 텍스트를 이용한다면 그림 1의 다른 부분도 똑같이 구현할 수 있다.

    누군가는 텍스트마저도 미리 준비된 이미지로 대처할 수 있지 않을까 생각할 수도 있다. 틀린 생각은 아니지만 언어, 폰트(Font) 등 시스템 설정에 맞게 텍스트가 유연하게 변경되기 위해서는 텍스트를 이미지로 출력하는 것은 여러 측면에서 한계가 많다.

    안드로이드, IOS, 윈도우와 같은 최신 플랫폼에서 제공하는 UI 기능을 살펴보면, 사실 앱 개발자는 이와 같은 원시적인 방법으로 UI를 구현하는 것은 상상할 수 없다. 사실 모든 플랫폼의 UI 프레임워크는 화면을 구성하는 공통된 기능과 특성을 UI 컨트롤(또는 위젯)로서 정의하여 제공하며 프로그래밍 관점에서 UI 컨트롤은 UI 객체(UI Object)로서 통용된다.

    그림 4: 다양한 종류의 UI 컨트롤

    앱 개발자는 UI 프레임워크에서 제공하는 UI 컨트롤를 조합하여 보다 쉽고 빠르게 다양한 앱 화면을 구축할 수 있다.

    //검색 상자 UI 컨트롤
    UISearchBox searchBox = new UISearchBox();             //검색 상자 생성
    searchBox.text(“Search Google or type URL”);           //가이드 텍스트 설정
    searchBox.icon(“./../VoiceRecognition.png”);           //음성 아이콘 설정
    searchBox.move(80, 300);                               //검색 상자 위치
    searchBox.resize(350, 50);                             //검색 상자 크기
    searchBox.show();                                      //검색 상자 출력하기
    

    코드 2: UI 컨트롤을 이용한 앱 화면 구성

    같은 목적을 수행하는 코드 1과 코드 2를 비교해 보면 구현이 얼마나 간단해 지는지 알 수 있다. 하물며 UI 컨트롤은 화면을 출력하는 기능 뿐만 아니라, UI 컨트롤과 사용자와의 상호작용을 위한 기능 동작까지 제공한다. 검색 상자에 사용자가 입력한 텍스트가 실시간으로 출력되는 기능을 생각해본다면, 앞서 이미지와 텍스트를 이용한 원시적 구현 방식과는 비교할 수 없을 정도로 구현 분량이 축소된다.

    사용자로부터 단순한 클릭 입력을 받는 버튼을 예로 들어보자. 버튼은 사용자의 클릭 이벤트를 전달받고 앱에게 그 상태를 전달한다. 버튼은 클릭이라는 이벤트를 가시적으로 표현하기 위해 클릭에 대한 상태 정의(Normal 상태, Press 상태) 및 각 상태별 이미지를 출력할 수 있어야 한다. 만약 상태 전이간 애니메이션까지 존재한다면 버튼의 구현은 훨씬 더 복잡하다.

    그림 5: 버튼의 클릭 상태 전이

    원시적 방식을 이용하여 버튼의 기능을 모두 구현한다면, 구현량은 물론 개발 난이도까지 급상승한다. 기본적으로 UI 컨트롤은 앱이 작성해야 할 코드 분량을 줄여주는 것은 물론, 룩앤필(Look & Feel)을 갖춘 테마 특성, 애니메이션, 이벤트, 예상치 못한 사용자 입력 처리 등 생각보다 많은 작업을 대신 정의하고 구현한다.

    일반적으로 UI 컨트롤의 동작 및 룩앤필의 특성은 프레임워크에서 정의한 테마에 따라 다르다. 달리 말하면, 앱이 어떤 UI 프레임워크를 기반으로 작성되었느냐에 따라 앱의 UI 특성은 완전히 달라진다. 게다가, 각 UI 프레임워크가 갖춘 다양한 테마에 따라 앱의 그래픽 출력 결과도 완전히 달라질 수 있다.

    그림 6: 테마에 따른 UI 컨트롤의 룩앤필 차이

    그림 7: 테마에 따른 동일 앱 UI의 룩앤필 차이

    만약 제공되는 UI 컨트롤 중 디자인한 앱과 부합하지 않거나 또는 필요한 UI 컨트롤이 존재하지 않는다면 코드 1과 같은 방식으로 이미지와 텍스트를 가지고 화면을 직접 구성할 수 있다. 프로토타입, 테스트 목적 등 앱의 완성도 및 호환성이 그다지 중요하지 않는 상황이라면 충분히 고려할 만하다. 그렇지 않다면, UI 컨트롤의 테마를 직접 수정하거나 새로 작성하는 방법을 고민해 볼 수 있다. 일반적으로 UI 프레임워크에서 제공하는 UI 컨트롤 테마 커스터마이징 기능은 기본 사항에 해당된다.


    3. 첫 번째 예제: 버튼과 이벤트 핸들링

    앞 절에서는 우리는 UI 컨트롤이 무엇인지 대략적으로 살펴보았다. 이번 절에서 우리는 지구 상에서 가장 단순한 UI 컨트롤을 하나 이용해 볼 예정이다. 이 예제로부터 우리는 UI 컨트롤과 앱간의 상호작용을 어떤 식으로 구현할 수 있는지 살펴볼 것이다.

    그림 8: 버튼

    버튼(Button)은 어떤 UI 프레임워크를 막론하고 동일하게 제공되는 UI 의 기본 형태 중 하나이다. 사용자는 버튼을 클릭함으로써 앱에게 신호를 전달한다. 앱은 버튼이 사용자로 하여금 클릭되었는지 신호를 감지할 수 있으며 그 신호에 상응하는 어떤 적절한 동작을 취할 수 있도록 구현을 한다. 이 예제에서는 버튼이 선택되었을 때 버튼 메세지를 변경한다.

    그림 9: 버튼 선택 메세지

    버튼을 앱 화면에 배치하기 위해 앱 개발자는 대략 다음과 같은 버튼 생성 코드를 작성할 수 있을 것이다.

    UIButton myBtn = new UIButton();    //버튼 생성
    myBtn.text(“My Button”);            //버튼의 출력될 텍스트
    myBtn.move(50, 50);                 //버튼 위치
    myBtn.resize(100, 100);             //버튼 크기
    myBtn.show();                       //화면에 나타내기
    

    코드 3: 버튼 생성

    화면에 버튼을 추가했지만, 사용자 입력 신호는 어떻게 처리할 수 있을까? 프로그래밍 관점에서 보면, 이벤트 리스너(Event Listener) 혹은 콜백 함수(Callback Function)와 같은 메커니즘을 이용하면 가능하다. 버튼이 클릭된 시점에 앱 개발자가 등록한 함수가 호출된다면, 앱 개발자는 해당 함수 내에서 원하는 동작을 추가 구현할 수 있다.

    //버튼에 clicked 이벤트 핸들링을 추가한다.
    myBtn.addEventCb(UIButton.CLICKED,
                     //CLICKED 이벤트 발생 시 아래 코드가 수행된다.
                     lambda(UIObject obj)
                     {
                         myBtn.setText(“Button Pressed”);
                     }
                    );
    

    코드 4: 클릭 이벤트 등록

    앱 개발자는 해당 이벤트 함수가 어떤 과정을 통해 호출되는지 전혀 알지 못한다. 하지만, 버튼이 클릭되었을 때 반드시 이벤트 함수가 불린다는 사실을 보장받아야 한다.

    눈치챘겠지만, UI 컨트롤을 잘 활용하기 위해서는 앱 개발자는 해당 컨트롤이 제공하는 이벤트의 종류를 이해할 수 있어야 한다. 버튼의 경우 “클릭” 이벤트를 제공하지만, 추가로 “눌림(PRESSED)”, “눌림 해제(UNPRESSED)”, “롱프레스(LONGPRESSED)” 와 같은 다른 이벤트도 제공할 수 있다. 앱 개발자는 각 이벤트에 대한 명세 내용을 정확히 이해하고 사용자 시나리오에 맞게 그 기능을 잘 활용할 수 있다.

    myBtn.addEventCb(UIButton.PRESSED,
                     //PRESSED 이벤트 발생 시 아래 코드가 수행된다.
                     lambda(UIObject obj) { ... }
                    );
    
    myBtn.addEventCb(UIButton.UNPRESSED,
                     //UNPRESSED 이벤트 발생 시 아래 코드가 수행된다.
                     lambda(UIObject obj) { ... }
                    );
    
    myBtn.addEventCb(UIButton.LONGPRESSED,
                     //LONGPRESSED 이벤트 발생 시 아래 코드가 수행된다.
                     lambda(UIObject obj) { ... }
                    );
    

    코드 5: 버튼에 여러 이벤트 등록

    한편, UI 프레임워크에서는 각 UI 컨트롤에 대한 정확한 동작 정의는 물론, 해당 기능을 앱 개발자가 이해할 수 있도록 명확한 인터페이스 설계 및 문서화가 필수적이다.

    /**
     * @defgroup UIButton Button
     * @ingroup UIFramework
     *
     * This is a push-button. Press it and run some function. It can contain
     * a simple label and icon object and it also has an autorepeat feature.
     *
     * This widget inherits from the @ref Layout one, so that all the
     * functions acting on it also work for button objects.
     * …
     * This control emits the following signals, besides the ones sent from Layout.
     * @li CLICKED: the user clicked the button (press/release).
     * @li PRESSED: button was pressed.
     * @li UNPRESSED: button was released after being pressed.
     * @li LONGPRESSED: the user pressed the button without releasing it.
     * ...
    

    코드 6: Doxygen 형식을 따른 버튼 문서화 예

    UI 프레임워크는 서로 다른 UI 컨트롤일지라도 유사한 동작의 경우 동일한 인터페이스를 갖추는 것이 앱 개발자로 하여금 보다 빠른 이해에 도움이 된다. 앱 개발자는 하나를 배움으로써 다른 UI 컨트롤의 동작도 유추할 수 있을 것이다.

    UIRadio myRadio = new UIRadio();    //라디오 생성
     
    //앱 개발자는 라디오를 잘 모를지라도, CLICKED 동작이 무얼 의미하는지는 유추할 수 있다.
    myRadio.addEventCb(UIRadio.CLICKED,
                       //CLICKED 이벤트 발생 시 아래 코드가 수행된다.
                       lambda(UIObject obj) { ... }
                      );
    

    코드 7: 동일한 이벤트 인터페이스의 예

    하나의 UI 컨트롤에 이벤트 처리를 추가함에 있어서 콜백 함수의 복수 등록은 충분히 가능하다. 버튼이 클릭되었을 때, 한편으로는 메세지를 출력하고 한편으로는 이미지를 출력할 수 있을 것이다. 두 동작을 하나의 함수 내에서 처리할 수도 있지만 서로 다른 개별 동작은 코드 관점에서 분리하여 작성하는 것이 코드 복잡도 측면에서 더 낫다. 결과적으로, 이벤트 처리도 복수 등록이 가능하도록 인터페이스를 설계해야 앱 개발이 더욱 편리하다.

    myBtn.addEventCb(UIButton.CLICKED,
                     //CLICKED 이벤트 발생 시 아래 코드가 수행된다.
                     lambda(UIObject obj) {    //메세지를 출력한다. }
                    );
    
    myBtn.addEventCb(UIButton.CLICKED,
                     //CLICKED 이벤트 발생 시 아래 코드 역시 수행된다.
                     lambda(UIObject obj) {    //이미지를 출력한다. }
                    );
    

    코드 8: 이벤트 중복 등록 예

    이벤트가 중복 등록된 경우, 어느 이벤트 함수가 선호출되어야 하는지 UI 프레임워크는 명확한 정책을 제시해야 한다. 앱 로직의 순서가 완전히 달라질 수도 있기 때문이다. 나중에 등록된 함수가 먼저 호출되는 경우가 일반적이지만, 상황에 따라 이러한 순서를 임의로 결정할 수 있는 이벤트 함수가 필요할 수도 있다.

    myBtn.addEventCb(UIButton.CLICKED, 
                     lambda(UIObject obj) { ... },
                     2,    //우선순위: 2
                    );
    
    //위 이벤트보다 우선순위가 더 높기 때문에 여기 등록된 이벤트 함수가 먼저 불린다.
    myBtn.addEventCb(UIButton.CLICKED,
                     lambda(UIObject obj) { ... },
                     1,    //우선 순위: 1
                    );
    

    코드 9: 이벤트 우선 순위 지정

    사실, 이벤트 함수의 순서에 의존하는 앱의 이벤트 처리 로직이 존재한다면 개선 여지가 있는지 다시 검토해 보아야 한다. 예로, 클릭 이벤트에 복수 이벤트 처리가 등록된 경우 각 이벤트 처리는 서로 독립적이어야 앱 로직의 복잡도를 줄일 수 있다. 각 이벤트 처리간 의존성이 존재한다면 이는 하나의 이벤트 처리로 합치는 것이 더 바람직하다.

    반면, 어떤 상황에서는 동작을 취소하기 위해 등록한 이벤트를 제거해야 할 수도 있다.

    ...
    //이벤트 콜백을 등록한다. 이후 이벤트 콜백을 해지하기 위해 콜백 핸들을 따로 보유한다.
    UIEventCb myEventCb = myBtn.addEventCb(UIButton.CLICKED, ...);
    ...
    //앞서 등록한 이벤트 콜백을 해지한다.
    myBtn.delEventCb(myEventCb);
    ...
    

    코드 10: 등록한 이벤트 삭제

    사용자 조건이 만족하기 전까지 앱의 특정 기능이 비활성화되어 있는 경우도 있다. 이 경우 해당 기능을 트리거하는 UI 컨트롤이 비활성화 상태로 존재해야 한다. 해당 컨트롤을 비활성화하는 기능을 제공하면 사용자로 하여금 해당 기능을 사용할 수 없다는 점을 인지할 수 있게 도와준다. 비활성화된 UI 컨트롤은 기본 동작을 수행하지 않음은 물론 관련 이벤트 역시 발생하지 않는다.

    그림 10: 버튼 비활성화

    myBtn.disable();    //버튼을 비활성화한다. 다시 활성화하려면 enable()을 호출하자.
    

    코드 11: 버튼 비활성화

    비활성화된 버튼은 UI 외양이 달라졌을 뿐만 아니라, 클릭과 같은 기본 기능 역시 동작하지 않는다.


    4. 앱 기본 구조 및 동작 분석

    앞 절에서 우리는 UI 컨트롤을 사용하여 앱과 사용자간의 상호작용하는 방법을 어떤 방식으로 구현할 수 있는지 간단한 예제를 통해 살펴보았다. 이번 절에서는 UI 컨트롤을 사용하는 앱이 UI 프레임워크와 어떻게 연동되어 동작할 수 있는지 간략히 알아보고자 한다. 이번 절 학습을 통해 UI 프레임워크와 앱 코드간의 기본 연동 방식 및 그 원리를 이해할 수 있을 것이다.

    C, C++, 자바 등 현대의 대표적인 프로그래밍 언어에서는 우리는 main() 함수에서 프로그램이 시작됨을 알고 있다. UI 컨트롤의 기능을 사용하기 위해 일반적으로 앱 프로세스는 UI 프레임워크의 엔진을 초기화하고 가동하는 작업을 수행해야 한다. 엔진이라고 하면, UI 컨트롤이 동작하는 핵심 기능을 수행하는 모듈이라고 볼 수 있다. 앱 개발에 있어서 엔진의 내부 동작 원리를 모를지라도 큰 문제는 안되지만, 앱 개발자가 엔진의 동작 원리를 이해한다면 문제 해결 및 앱 최적화 측면에서 큰 도움이 될 수는 있다. 반면, 사용하기 좋은 UI 프레임워크일수록 앱 개발자는 엔진 내부의 동작 방식에 영향을 받지 않고 조금 더 자유로운 방식으로 쉽고 빠르게 앱을 개발할 수 있어야 한다. 일반적으로 UI 엔진은 앱의 로직과는 별개로 앱의 그래픽 출력을 위해 복잡한 연산 및 로직을 무대 뒤에서 열심히 수행한다.

    /*
     * UIEngine은 UI 컨트롤의 기능을 구동하는 모듈이다.
     * UIEngine이라는 명칭은 임의로 정함. 
     * 실제로는 UI 프레임워크 및 모듈의 실제 이름 등의 더 적절한 이름을 요구한다.
    */
    main()
    {
        UIEngine.init();    //엔진 초기화
        UIEngine.run();     //엔진 가동. 내부적으로 메인루프(MainLoop)가 가동한다.
        UIEngine.term();    //엔진 종료
    }
    

    코드 12: UI 엔진 초기화 및 가동

    코드 12는 UIEngine을 초기화, 가동, 종료하는 코드이다. init()에서는 UI 엔진이 사용하는 리소스를 불러오고 엔진이 적절히 구동되기 위한 준비 작업을 수행한다. run()에서는 준비된 리소스를 가지고 실제 엔진을 가동한다. run()이 호출된 이후에는 엔진이 종료 요청을 받기 전까지 run()은 메인루프를 통해 계속 가동되야만 한다. 만약 run() 메서드가 종료된다면 앱 프로세스 역시 main() 함수와 함께 종료될 것이다. 이 경우, 앱의 UI는 화면에서 지속될 수가 없다. run() 내부적으로 메인루프가 가동되면서 매 루프마다 지정된 어떤 작업을 수행한다. term()는 run()이 끝난 앱의 종료 시점에 호출되며 엔진에서 사용한 리소스를 모두 정리하는 작업을 수행한다. 엔진은 사용자가 사용한 UI 컨트롤 등의 리소스가 해제되어 있지 않으면 term() 호출 시점에 내부적으로 알아서 정리해 줄 수 있다.

    일부 플랫폼에서 코드 12와 같은 UI 엔진을 초기화, 가동, 종료하는 호출이 없다고 놀랄 필요가 없다. 일반적으로 앱이 구동되는 여러 플랫폼에서는 UI 엔진을 더욱 추상화한다. 플랫폼은 UI 엔진의 존재를 감추고 앱 개발자로 하여금 필요한 UI 컨트롤을 바로 호출할 수 있도록 코드 템플릿을 제공하거나 애플리케이션 프레임워크를 좀 더 보완하여 제공한다. 결과적으로, main() 함수에서 직접적으로 UI 엔진을 초기화, 가동, 종료하는 코드는 사용되지 않을 가능성이 크다. 일반적으로 앱은 UI 엔진 뿐만 아니라 사운드, 네트워크 등의 기타 라이브러리 및 엔진 그리고 서비스를 동시에 사용하기 때문에 엔진의 초기화 작업 및 가동은 앱 프레임워크 내부에서 일괄적으로 처리해 줄 수 있다. 코드 13은 이러한 부분을 추상화한 UIApp 클래스를 이용하는 예이다.

    main()
    {
        UIApp.init();
        UIApp.run();
        UIApp.term();
    }
    
    /*
     * 애플리케이션 프레임워크에서 제공하는 기능이며 클래스 및 메서드 이름은 임의로 정했다.
    */
    UIApp
    {
        //UI 엔진 뿐만 아니라 여러 라이브러리 및 서비스를 초기화 한다.
        init()
        {
            ...
            UIEngine.init();   
            ...
        }
    
        /* UIEngine.run() 전후로 여러 기능이 수행될 수 있지만 UI 엔진을 가동하는 것이
           핵심이다. */
        run()
        {
            ...
            UIEngine.run();
            ...
        }
    
        //마찬가지로 UI 엔진 뿐만 아니라 여러 라이브러리 및 서비스를 종료한다.
        term()
        {
            ...
            UIEngine.term();
            ...
        }
    }
    

    코드 13: UIApp 클래스를 활용한 main() 작성

    UIApp.run()까지 잘 호출되었다고 가정하자. 앱이 처음 가동되면 앱은 첫 화면으로 무언가를 보여주어야 한다. 2.3절에서 살펴본 버튼 예제와 같은 방식으로 여러 UI 컨트롤을 배치한다면 화면 구성이 가능하다. 다만, 그전에 우리는 UI 컨트롤을 배치할 앱의 윈도우(Window)를 하나 생성해야 한다. 사실, 윈도우는 플랫폼마다 그 특성이 다르긴 한데 안드로이드의 경우에는 하나의 앱이 여러 뷰(View)를 보유할 수 있으며 각 뷰다 윈도우가 할당되는 반면, MS 윈도우나 리눅스의 전통적인 X Window 시스템에서는 일반적으로 하나의 앱이 하나의 윈도우를 보유하며 윈도우 내에서 앱이 마음대로 뷰를 구성한다. 하지만, 지금은 윈도우가 디바이스 화면에서 앱이 출력될 위치 및 크기를 결정하는 출력 영역 정도로 이해해도 좋다. 일반적인 데스크탑 환경을 이용해 보았다면 윈도우의 개념 이해는 크게 어렵지 않을 것이다.

    그림 11: 데스크탑 환경의 윈도우

    앱의 첫 화면을 위해 윈도우를 생성하고 UI 컨트롤을 배치하는 작업은 run()의 무한루프가 본격적으로 수행되기 이전에 수행되어야 할 것이다. 그렇다면, UIApp.init(), UIApp.run() 사이에서 수행하면 될까? UIEngine을 직접 사용한다면 init()과 run() 사이가 맞지만 UIApp 기반에서는 init()과 run() 내에서 여러 추가적인 작업들이 수행될 수 있기 때문에 UIApp 클래스는 사용자에게 UI를 생성할 시점을 알려주는 것이 더 명확하다. 사실 이 부분은 앱의 라이프사이클(Life-Cycle)과도 관련이 있는데, 모바일 환경의 앱의 경우 리소스 제약이나 사용 환경의 특성으로 인해 앱이 시스템의 지배를 더 많이 받기 때문이다. 그렇기 때문에, 애플리케이션 프레임워크는 앱 개발자로 하여금 앱의 라이프사이클에 맞는 동작을 수행할 수 있는 인터페이스를 기본적으로 제공한다. 앱이 UI를 구성해야 하는 시점 역시 이러한 라이프사이클의 일부분으로서 존재할 수 있다.

    그림 12: 앱 라이프사이클 모델

    UIWindow myWnd;     //윈도우 객체를 보관할 인스턴스
    
    /*
     * MyAppLifeCycle은 AppLifeCycle을 상속해서 구현한다.
     * AppLifeCycle은 앱의 라이프사이클 인터페이스를 제공한다.
     * 이 예제에서는 대표적인 4개의 상태만 언급한다.
    */
    MyAppLifeCycle : AppLifeCycle
    {
        /*
         * create()는 앱이 최초 생성될 경우 호출된다.
         * 앱 개발자는 여기서 첫 화면을 구성한다.
        */
        create() override
        {
            myWnd = new Window();             //윈도우 생성
            myWnd.title(“My Window”);         //윈도우의 타이틀
            myWnd.resize(400, 400);           //윈도우 크기
    
            //윈도우 생성 후 필요한 UI 컨트롤을 추가적으로 생성한다...
    
            UIButton myBtn = new Button(myWnd);    //myWnd의 버튼을 생성한다.
            myBtn.text(“Exit”);                    //버튼의 출력될 텍스트
            myBtn.move(50, 50);                    //버튼 위치
            myBtn.resize(100, 100);                //버튼 크기
            myBtn.show();                          //화면에 나타내기
            myBtn.addEventCb(UIButton.CLICKED,
                             lambda(UIObject obj)
                             {
                                 UIApp.exit();    //버튼 클릭시 앱을 바로 종료한다.
                             },
                            );
        }
    
        /*
         * destroy()는 앱 종료를 요청받을 경우 호출된다.
         * 사용자에 의해 윈도우의 종료 버튼이 눌리는 경우 등이 이에 해당된다.
         * 앱 관리자에 의해 강제 종료되는 경우도 해당된다.
         * 여기서 앱은 사용한 리소스를 정리한다.
        */
        destroy() override
        {
            /* 앱이 종료될 시 생성한 윈도우를 제거해준다.
               UIEngine의 term()에 의해 자동으로 수행될 수 있으므로 사실 필수는 아니다. */
            myWnd = null;
        }
    
        /*
         * pause()는 앱이 백그라운드(Background)로 전환되거나 일시정지될 경우 호출된다.
         * 윈도우 최소화, 창 전환 등의 경우에 해당된다.
         * 갑자기 걸려온 전화로 전화 앱이 구동되는 경우에도 해당된다.
         * 애니메이션 등 불필요한 과도한 출력 처리는 여기서 정지시키는 것이 좋다.
        */
        pause() override
        {
            myWnd.hide();    //윈도우를 숨긴다... 꼭 필요할까?
        }
    
        /*
         * resume()는 앱이 포어그라운드(Foreground)로 다시 전환되는 경우 호출된다.
         * 앱이 최초 생성된 후 가동될 경우에도 해당된다.
         * 일시정지했던 작업이 있다면 여기서 다시 재개시킨다.
        */
        resume() override
        {
            myWnd.show();    //윈도우를 나타낸다… 꼭 필요할까?
        }
    }
    
    main()
    {
        //라이프사이클 정보를 UIApp를 통해 앱 프레임워크 코어쪽으로 전달한다.
        MyAppLifeCycle lifeCycle = new MyAppLifeCycle();
        UIApp.init(lifeCycle);
        UIApp.run();
        UIApp.term();
    }
    

    코드 13:앱 라이프사이클에 따른 UI 생성

    주석을 통해 코드에 대한 전반적인 이해는 가능할 것으로 기대한다. 몇 부분만 추가 설명하자면, 우선 24줄을 보면 버튼 생성 시 윈도우 객체를 전달하는 것을 확인할 수 있을 것이다. 생성하는 버튼이 어느 윈도우에서 출력되어야 하는 버튼인지를 지정하기 위해 추가한 사항이다. 하나의 앱이 여러 개의 윈도우를 동시에 보유할 수 있다는 가정하에 필요하다고 판단하여 추가하였다. 두 번째로 32줄을 보면 UIApp.exit()를 통해 앱 종료를 요청하는 작업을 볼 수 있는데 실제로 UIApp.exit()는 UIEngine으로 하여금 메인루프를 중단하는 작업을 요청할 것이다.

    UIApp.exit()
    {
        ...
        UIEngine.stop();     //UIEngine의 메인루프를 중단한다.
        ...
    }
    

    코드 14: UIApp.exit()의 코드

    마지막으로, pause()와 resume()에서 윈도우를 숨기고 나타내는 작업을 앱 개발자가 직접 수행하고 있는데 과연 이러한 호출이 필요할까 의문일 수도 있다. 만약 앱 개발자가 직접 호출을 수행해야 한다면, Pause 시 악의적으로 윈도우를 숨기지 않을 수도 있기 때문에 사실 이 부분은 앱의 역할이라기 보다는 윈도우를 관리하는 윈도우 관리자(Window Manager)가 수행해야 할 작업이 더 맞을 듯 보인다. 아직까지는 윈도우 관리자에 대해서 설명하지 않았으므로 윈도우 관리자가 다소 생소한 독자들을 위해 앱 개발자가 직접 호출하도록 코드를 남겨두었다.

    실제로 플랫폼에서 제공하는 앱 사이클의 정의 및 시나리오는 본 예제보다 다소 복잡할 수 있다. 여기서는 라이프사이클 모델을 최대한 단순화하여 이 정도로만 언급하고 넘어가도록 한다. 다음 그림은 실제 윈도우10 UWP 앱의 라이프사이클을 보여준다.

    그림 13: 윈도우10 UWP(Universal Windows Platform) 앱의 라이프사이클


    마지막으로, 이쯤해서 UIEngine의 메인루프를 도식화해보자. 실제 메인루프 내에서 수행해야 하는 작업들은 훨씬 더 복잡할테지만 지금까지는 대략 다음과 같은 작업을 수행할 것이다.

    그림 14: UIEngine 메인루프


    UIEngine의 메인루프는 매 루프마다 사용자의 입력을 비롯한 새로운 이벤트가 발생했는지 확인한 후, 이벤트가 존재한다면 이벤트를 처리한다. 그리고 이벤트에 의해 UI 컨트롤과 같은 UI 객체에 변화가 발생했는지 확인한 후, 변화가 있다면 이를 반영하여 새로운 렌더링(Rendering) 작업을 수행하고 최종적으로 앱의 화면을 재갱신한다. 이러한 절차의 작업은 UIEngine.stop()이 호출될 때까지 끊임없이 반복된다.


    5. 정리하기

    이상으로, 우리는 앱 개발에 필요한 UI 프레임워크의 기본 기능과 그 개념을 간략하게 살펴보았다. UI를 구성하는데 있어서 가장 원시적인 방법으로 이미지와 텍스트를 이용하는 방법이 있으며 UI 컨트롤을 이용하면 더욱 쉽고 빠른 UI 개발이 가능함을 알 수 있었다. UI 컨트롤은 단순히 그래픽 출력 뿐만 아니라 사용자와 앱간의 상호작용이 가능한 기능을 제공하며 이러한 기능을 위해 앱 개발자는 UI 컨트롤이 제공하는 이벤트를 처리하는 방법이 필요함을 알 수 있었다. 앱은 기본적으로 윈도우를 하나 생성하여 출력 영역을 확보하고 윈도우 안에 UI를 구성함을 알 수 있었고 앱 라이프사이클에 맞춰 UI 생성 및 조작이 필요함을 알 수 있었다. 마지막으로 이러한 UI가 가동될 수 있는 UI 엔진과 메인루프에 대한 기본 개념도 함께 살펴보았다.


    저작자 표시 비영리 변경 금지
    신고


    Material: 4B&H pencil, Sketchbook

    저작자 표시
    신고

    Inlist, which is a sort of the Linked List, is an optimized linked list for the data structure and it's behavior performance. The key point of Inlist is, it embeds a user data in its own data structure, does not have a user data separately. Let's see an example quickly.

    Firstly, let me describe a normal Linked List data structure. We can suppose that data structure would be provided with an API like a useful library.

    //A list node information. data field points actual user data.
    struct ListNode {
        ListNode *prev;
        ListNode *next;
        void *data;
    };
    //A data structure for accessing list nodes. struct List { ListNode *head; ListNode *last; };

    It's just a common Linked List data structure. No problems even though no explanation. See next.

    //Create a new list.
    List* list_create() {
        return (List*) calloc(1, sizeof(List));
    };
    //Append a new item(node) in a list. bool list_append_item(List* list, void *data ) { if (list == null) return false; //Create a new node. ListNode *node = (ListNode*) calloc(1, sizeof(ListNode)); if (node == null) return false; node->data = data; //In case of the first node. if (list->last == null) { list->head = list->last = node; return true; } //Append a new node in the list. ListNode *last = list->last; last->next = node; node->prev = last; list->last = node; return true; }


    list_create() generates a list and the other one appends a new node in the list. I believe you already know Linked List so we won't look at the above code in detail. (Just in case, you can easily find a Linked List concept by googling.)

    So far, it looks nice. We can provide a sort of free functions with regard to the above additionally but I'd like to skip them because they are actually at outside of the stake.

    Then, we can suppose a user uses our functions in this scenario.

    //User data structure
    struct UserData {
        int idx;
        int val;
    };
    
    void main() {
    
        List *list = list_create(); 
     
        //Set up a list.
    
        //Create arbitrary 100 items. Skiping here, but create_userdata() creates a UserData data and returns it.
        for (int idx = 0; idx < 100; ++idx) {       
           UserData *usrdat = create_userdata(idx, idx * 100);
           list_append_item(list, usrdat);
        }
     
        //Verify that list is correct or not.
    
        //It would be better if we provide a function, list_foreach(), to iterate a list...
        ListNode *node = null;
        UserData *usrdat = null;
    
        list_foreach(list, node,  usrdat, UserData) {
            if (usrdat)
                printf("%d %d\n", usrdat->idx, usrdat->val );
        }
    
        //Skip the free sequence..
    }
    

    Seems very well. But for some people who are likely to ask me how to implement list_foreach(), I'm adding the function code here.

    #define list_foreach( list, node, usrdat, DATA_TYPE ) \
        for (node = list->head, usrdat = (DATA_TYPE*) _get_usr_data(node); node; node = _prev_get_next(node), usrdat = (DATA_TYPE*) _get_usr_data(node))
    


    The code won't be the perfect however at least, we can imagine such that code we can define.

    _prev_get_next() is an internal function which returns a next node, _get_user_data() is an internal function which returns user data from a node. Both of them actually are not important, we don't need to waste time by diving them to dig. So let's skip them.

    So far, we've looked a normal Linked List and its peripheral functions usage. Here point we have to notice again is the next sequence that builds up a working Linked List.

    1. Create a list(list_create()).
    2. Create a user data(create_userdata())
    3. After creating a node, store user data in that node(list_append_item()).
    4. Append a new node in the list(list_append_item()).

    By this time, if we see a figure of its structure, it must be looked like this.

    The key point here is, on building the structure, it needs to allocate 2 pieces of fragmented data memory per one item. Plus, every loop, it requires referring pointers, node->data, to access user data.

    Then, let's take a look at the difference with Inlist. Inlist reduces memory allocation count as well as pointer access count by merging Node and UsrData like the next figure.


    Someone may think, it's a piece of cake. If user implements the list manually, they could have implemented it like the above. On the other hands, if you provide the list function to users or implement it internally as a re-usable function for yourself, you are possibly somewhat impressed.

    Now, we understand the concept of the Inlist. Let's start to implement it quickly. I will modify the previous list code and here I will show you the just different parts of the code. Let's take a look at UsrData first.

    #define LISTNODE ListNode node;  //Define for user convenience.
    
    //User data structure
    struct UserData {
        //Firstly, adds a field using that macro.
        LISTNODE;
        int idx;
        int val;
    };
    


    Like above the figure, modified UserData to include ListNode data fields. Next, let's modify List and ListNode.

    struct ListNode {
        ListNode *prev;
        ListNode *next;
        void *data;  //No more use.
    };
    
    struct List {    
        //Nothing changed.
        ListNode *head;
        ListNode *last;
    };
    

    Now, it doesn't need to allocate ListNode but build Linked List via UserData.

    //Append a new  item(node) in the list.
    bool list_append_item(List* list, void *data) {
    
        if (list == null || data == null) return false;
    
        //Create a new node. Not necessary anymore.
        ListNode *node = (ListNode*) calloc(1, sizeof(ListNode));
        if( node == NULL ) return false;
        node->data = data;
    
        //Convert to ListNode.
        //In fact, simply use typename to access list node from user data if it is C++...
        ListNode* node = (ListNode*) data;
    
        //In case of the first node.
        if (list->last == null) {       
            list->head = list->last = data;
            return true;
         }
    
        //Append a new node in the list.
        ListNode *last = list->last;
        last->next = node;
        node->prev = last;
        list->last = node;
    
        return true;
    }
    

    No big changes, but just use void* type data(exactly for UserData) instead of the ListNode* to construct the list.

    Lastly, let's take a look at the code which describes the iterator body of the list.

    #define list_foreach(list, usrdat) \
        for (usrdat = list->head, usrdat; usrdat = _prev_get_next(usrdat))
     
    void main() {
     
        //Create a list and set it up here...
    
        UserData *usrdat = null;
        list_foreach(list, usrdat) {
            printf("%d %d\n", usrdat->idx, usrdat->val);
        }
    }
    


    You might be noticed that it is simpler than previous one because it doesn't need to access a user data from a node anymore. But, of course, it has a con that user needs to declare LISTNODE field in the first line of the UserData structure. But actually it is not big deal. Other than that, we can still provide utility functions for user convenience.

    So far, we've taken a look at the Inlist. It is a compact data structure across the nodes, also it's possible to access to user data faster than normal version. But it must be used when the data is designed to work along with the list. Actually, this Inlist concept was introduced in Enlightenment opensource project years ago. If you are interested in it more than this, you can visit here to look the whole functionalities and its implementation bodies.


    저작자 표시
    신고

    I'm gonna talk about corner cases in my smartphone life in China, a worse than other situations. One of annoying stuff is some apps don't support the copy text function. For instance, Baidu(is a kind of google in China) app toggles a context menu when I do long press on the screen. 



    In the context menu, there is not a text copy item. Sure, I can use other web browsers to avoid this suck (yay, good bye!), But just curious why they don't support the copy text? Actually, from Baidu web-surfing, I could find a bunch of users asked about this similar situations, they seemed be annoying about this strange corner.  


    This is not only the Baidu web app problem but other apps also do. For example, Baidu Map, Didichuxing(a kind of Uber) are one of the essential tools for my daily life in China. Practically, a lot of citizens and tourists also depend on these apps. Let's jump into Baidu map.


    Baidu map


    This is worse. Of course Baidu map is not irreplaceable but still it is a representative map app in China. Somewhat it is better than other foreign companies maps (i.e, google map) because it is specialized in China region and data. When user searches a region, it suggests additional information such as most favorite restaurants, tourists attractions etc. That is not surprising, it is a common feature all over the map apps.


    Point here is, when I searched a good place and think to dig it more, I need to research it using a web browser. That means, I need to copy one of information-address, phone number, store name, region name, etc- and then paste in the search box of the browser. But, It doesn't allow me to copy this information. Oops, you know, Chinese is very difficult to type if you don't know how to read the Chinese characters. First time, it is outrageous. It is very annoying when I could not read them. In the end, I give up searching and go back to the google map because English is better than Chinese to me. Of course, we can use Chinese dictionary then find how to read the Chinese characters and then research it. But I'm sure it is also horribly inconvenient.


    Now, question, why they don't allow us to search the characters? I imagine two scenarios.


     A. intentional purpose (for contents protection)

     B. technical issue.

     C. design problem (Is it considerable? Or just have no idea why they need to support it)


    At least, I'm sure there is not an intentional purpose because that information is not serious data at all. Also, when you use a browser using desktop PC, it still allows user to copy text information. So, it just leads to A or B problems.


    In point of S/W development view, basically software platforms support a text widget or similar UI components which support text copy/paste function in default. If some text part or text view of the application doesn't enable the copy text function, I guess it probably uses an extra component, not the default one, for the text area. I don't like to talk to you it is wrong because we don't understand its background. However, I'm still curious why they don't support it, Is it difficult? Or they just think it is just a trivial function?


    I checked Google map and Naver map(a famous Korean map app) just in case. And then surprisingly I just realized Naver map doesn't support multi-language feature. Also, it doesn't support text copy function for whole text area but does only for some of them. Still, it is inconvenient for me but I think it's better than Baidu map.


    Naver map


    Then how about Google? Impressive! It supports not only multi-language but also text copy function.


    Support Multi-Language


    Copy text information


    If you see the above Google map figure, its copy text UI interface is not a default one. It seems one of the additional or extra ones(just my guess). So I am surprised because it means they intentionally added that feature for this user scenario that I encountered.


    Default copy and paste UI interface


    I'm not one google sucker but a little surprised by google. Because in China, people cannot use Google service but google apps still perfectly works for Chinese. (Of course it needs VPN)  


    Today, we checked one use-case even though a trivial one, but I'd like to say this, every software companies can develop similar software products but their quality and service won't be same. As if it is a kind of this, masterpiece or not. That comes from a difference of software design. When we design a software, do we consider user scenarios enough? Do we design a software for user convenient or just try to copy the prime one? It is clear that, with enough considering user scenarios to make them convenient, users definitely feel your software is better and feel a greater identity of your company.

    저작자 표시
    신고


    Material: 4B&H pencil, Sketchbook

    저작자 표시
    신고