[총선리뷰] 머리는 장식용임을 증명해준 여러분들에게

어차피 이런 글 블로그에 올려봐야 읽는 사람도 얼마 안될 거고, 설령 읽는 다 한들 1)이미 알아서 잘 하고 있는 사람이거나 2)백번 천번을 이야기해봐야 소용없는 사람이기 때문에 쓴다는 거 자체가 사실 무의미하긴하지만, 마지막 정치 드립이니 그냥 기록해 두는 차원으로 끄적인다.

먼저 뜬금없는 질문부터 하나 해 보자. ‘정치란 무엇인가?’ 그래, 정치가 뭐냐? 선거냐? 놀랍게도 이 질문의 답은 학교에서 가르친다. 교과서에 나온 문자 그대로의 정의를 외우지는 못하지만 “상충하는 이해를 조정하는 일” 정도였던 것 같다. 여기서 이해는 오해의 반대발이 아니라, 이익관계를 이야기하는 말이고.

그런면에서 우리의 선거의식은 심각한 수준으로 정치적이지 못하다. 이건 입진보를 포함한 진보 진영이 이에 대해서는 너무 방기했기 때문이라고본다. 개누리가 쳐놓은 도덕의 프레임에 갇혀서 놀아난 결과는 바로 지금 이순간에도 진행중이고. 암튼 뭐가 “정치적이지 못하냐”에 대해서 좀 이야기해보자.

정치는 결국 “내 이익을 최대화하기 위해” 애쓰는 모든 활동이다. 시장에 가서 콩나물값 깎는 것도 정치고, A/S 센터가서 노트북을 데스크에 패대기치는 행위도 과격하나마 정치적인 행위다. 이를 위해 내 이익을 관철시킬 수 있으니까 말이다. 이런 일상의 소소한 밀고 당김도 정치지만 더욱 중요한 것은 국가 시스템의 일부로 우리가 흔히 뉴스를 통해 접하는 ‘정치’이다.

우리나라는 기본적으로 법에 의해 동작하는 체계이다. (그게 지금 잘되고 안되고는 나중에 생각해볼 문제이고) 그리고 그 법을 만드는 기관이 국회이고 국회의원이다. 법치국가에서 법은 국가를 굴러가게하는 주요한 로직이 된다. 그런데 이 법은 대체로 “방향성”을 갖고 있다. 즉 “어떤 것은 이래야 한다.”라는 형태로 정의가 되기 때문이다. 표현이 복잡하고 어려운 말을 쓰지만, “사람을 함부로 죽여서는 안되며, 고의로 사람을 죽인 자는 처벌 받아야한다.”라는 게 법이 말하고 있는 규칙 중의 하나가 아닌가.

예로 들었던 이 살인과 관련된 규칙은 일견 단순히 ‘생명의 존엄’이나 ‘인간의 소중함’ 같은 걸 주장하는 것처럼 보이지만 말 그대로 “누군가의 이익에 부합’하기 위한 것이다. 대부분의 사람은 자신의 생명이 매우 소중하고 비싸다고 생각하므로 다른 사람에 의해 목숨을 잃는 것는 손해가 되고, 생명을 계속 유지해나가는 것이 이익이 된다. 하지만 아주 극한 예이기는 하지만 그 반대의 사람도 있다. 다른 사람의 목숨을 빼앗는 것이 이익이 되는 사람도 있다는 것이다. 살인 청부업자나 장기 매매와 같은 것을 업으로 삼는 사람들 말이다.

물론 이런 일은 “나쁜 짓”으로 규탄받을 수 있다. 중요한 건 “그게 다른 사람에게는 손해가 된다.”는게 핵심이고 그 ‘다른 사람’이 거의 대다수의 국민이기 때문에 그게 법으로 정해져서 금지되는 것이다.

시장에서 콩나물 값을 깎는 예를 다시 생각해보자. 콩나물을 파는 사람이 많아지고, 국회의원 중에서도 콩나물을 파는 사람이 많다고한다면, 법을 만드는 사람들은 자신에게 손해가 되지 않도록 콩나물 값을 깎는 행위를 막는 법을 만들면 된다. 이게 허무맹랑하게 들리는 이유는 다른데 있지 않다, 국회에 콩나물 파는 사람이 없기 때문이다. 좀 더 극단적으로 만약 장기매매가 진짜 돈이 되고 번창하는 사업이된다면 (수출도 막하고 말이지) 그럼 그런 사람 중에서 국회의원이 나와서 그걸 합법화하면 되는 거다. 어렵겠지만 헌법도 바꾸면 그만인 거고.

바로 이거다. 국가의 체계가 나의 이익에 도움이 되기 위해서는 나에게 유리하게 국가가 돌아가도록 만들면 된다. 가장 직접적으로는 내가 스스로 국회의원이 되면 된다. 그런데 나는 친구는 커녕 지인도 별로 없다. 그럼 어떡하면 좋은까? 당연히 나하고 제일 이해관계에서의 입장이 비슷한 사람을 국회의원을 만들면 된다. 정치에서의 답은 “내게 이익이 되도록 하는”것이 맞다.

혈연, 지연, 학연으로 투표하는 것. 욕할 수 없다. 왜? 나와 가까운 사람이니까 이 사람이 국회의원이 되면 다른 누구보다 내가 더 덕볼 수 있기 때문이다. 국회가 열릴 때마다 파행으로 치닫지만 꼬박꼬박 잘 통과되는 법안이 하나 있지. 뭔가? 바로 국회의원 월급 올리는 거다.

성인 군자가 국회에 가는 거 아니다. 국회의원으로 나가는 사람은 다른 누구의 이익도 아닌 스스로의 이익을 위해 노력한다. 여러분의 일꾼이 되겠다? 집어치우라 그래라. 보수와 진보를 떠나서 어떤 유세장에서든 국민의 머슴이 되고, 여러분을 위해 일한다는 소리는 개소리다. 국회에 나간 모든 이들은 목숨을 걸고 자신의 이익을 위해 싸운다.

목숨을 걸고 자신의 이익을 위해 싸우는 것. 이게 정치의 본질이다. “착한 사람이 국회의원이 되어야지”, “깨끗한 사람이 국회의원이 되어야지”. 이건 도대체 어디서 나온 이야기인가? 깨끗하고 정직한 사람이 국회에 가면 별로 할 수 있는 일이 없다. 기껏해봐야 “자신의 이익을 다른 사람에게 양보”하겠지. 왜? 착한 사람이라며.

조금 더 실질적인 예를 들어볼까? 자 환율이 오르면 월급쟁이들은 먹고 살기가 힘들어진다. 기름값부터 온갖 수입 원자재 가격이 올라가거든. 근데 우리 지역구에 나온 후보가 내 사촌형이다. 그런데 이 사촌형은 집안 대대로 손톱깎이를 만들어서 외국에 수출만하고 있다. 사촌형은 국회에 가면 환율을 올리는 쪽으로 국가 체계가 돌아가는데 일조할 것이다. 왜냐면 환율이 올라야 자신한테는 이익이거든? 그런데 당신은 아마 5년동안 먹고 싶은 거 사고 싶은 거 포기해야 하는게 많아질거다. 만약 차라도 굴리고 있다면 조금 더 골치아파질거고. 이때는 혈연지연이 무슨 소용인가. 당장 내 주머니가 가벼워지고 살림 살이가 빡빡해질텐데. 누굴 뽑아야 하는가?

이명박이 대통령으로 당선될 때 “고작 집값때문에 그랬냐”고 비아냥 거리는 말이 많았다. 그런데 이명박의 득표량은 대략 주택 소유자의 비율과 비슷하다. 집값이 뛸 거니까 그거 보고 투표한거라는 말이다. 문제는 이 사람들이 이명박을 대통령으로 만들지 않았다는 거다. 집이 없는대도 이명박을 찍은 사람, 그리고 집이 없는데 투표를 안한 사람들이 이명박을 만든 장본인이고, 국개론이 말하는 개새끼이다.

다시 한 번 원리를 말해주겠다. 정치는 “내 이익을 관철”시키는 활동이다. 따라서 국민을 대표하는 사람을 뽑을 때는 “국민을 위해 정의를 실현하고…’ 이런 건 다 사기고 내 이익을 대표하는 사람을 뽑는게 정답이다.

이게 이른 바 ‘힘있는 자들’의 핵심적 정치마인드이다. 이건 나쁜게 아니고 가장 정확하게 시스템을 파악하고 있다는 증거이고 그만큼 잘 활용하고 있는거다. 전국에서 투표율이 가장 높은 동네는 노년층이 많인 동네가 아니라 부자동네다.

그리고 트위터에서 보이는 제일 병신같은 트윗이 “XX동은 벌써 투표율이 몇 프로인데, 악착같이 그들의 이익을 위해 투표하는게 꼴사납다”류의 이야기이다. 못났다. 그건 “가장 모범적인 민주 시민의 자세”이다. 투표가 국가라는 시스템의 룰을 결정한다는 사실을 가장 잘 깨닫고 그걸 실천하기까지 한다. 이건 비난이 아니라 칭찬해야하고 본받아야 하는 사실이다. 그런데 이걸 말해주는 언론이나 교육기관은 없다. 무능한 야권 정치인들 역시 마찬가지다.

아니 생각해보건데, 민주통합당을 보라. 말은 자신들에게 표달라고 하는데, 그들은 잃을 게 없는 싸움이다. 그들 면면을 보면 한나라당을 찍어줘도 손해볼게 없는 사람들이다. 그런데 그들이 과연 힘없는 자들, 월급받아 쪼개쓰는 자들, 학비 대출 갚느라 등골이 휘는 자들의 이익을 어떻게 대변할 것인데?

기득권층은 상대적으로 수가 적다. 그래서 수 싸움인 투표에서는 어찌보면 불리하다. 그래서 기득권층은 ‘도덕성’ 프레임을 즐긴다. ‘나쁜 새끼’라는 욕을 먹으면 상대 진영은 그만큼 착하고 바르고 깨끗한 사람이어야 하는데, 그러면 그 사람이 정작 ‘누구의 이익을 대변해 줄 수 있는지’를 말할 여지가 줄어들기 때문이다. 그들이 표를 던질 곳은 명확하다. 그래서 이들이 싸움에서 이기려면 반대쪽의 ‘목표’를 흐릿하게 만들어버리는 것이고, 이번에도 이런 전략은 정말 잘 먹힌다.

민주당이 늘 하는 이야기는 ‘정권 심판’이다. 다른 이야기도 물론 한다. 그런데 유세장에 가보면 언제나 주제는 정권심판이다. 거기 나와있는 사람들을 갖고 놀겠다는 심산이다. 대체로 면면을 보면 이미 새누리당이 그들의 이익을 대변해 줄만한 사람들이다. 그러면서 자기는 그들의 대척점에 있기를 자처한다고 하며 그들을 벌해야 한다고 한다.

이제 좀 뭐가 잘 못 됐는지 감이 잡히나? 민주당 지도부가 무능하게 아니다. 국민이 너무 선거를 ‘정치적이지 못하게’하고 있는게 문제다. 그냥 머리가 나쁜거다. 통합 진보당이나 진보신당, 청년당은 왜 저모양으로 찌그러져 있어야 하는가? 소득 수준의 분포나 인구 수 등으로 봤을 때 이들 당이 1,2당을 해야 정상아닐까? 그걸 그렇게 만들지 못하는 원인은 곧 우리 나라 정치를 후진국 수준에 머물게 하고 있는 원인과 동일하고, 그 원인의 제공은 바로 국민 스스로가 하고 있다.

정치판에 도덕책 놀이는 필요없다. 대의도 필요없다. 대의는 정치인한테만 있으면 되고 국민에게는 필요없다. 민주국가가 실현해야하는 대의는 이미 헌법에 정해져 있고, 우리는 그저 대한민국 국민이기만 하면 된다.

그보다는 실질적으로 ‘내 이익을 누가 혹은 어떤 당이 대변해줄 것인가’를 생각해보라. 그리고 투표하라. 이 두 가지를 실천하지 못한다면, 어느 날 구청 직원들이 집으로 몰려와 당신과 당신 가족을 길바닥으로 내쫒는다고 한들, 아무 할 말이 없어지는 거다. 자신의 이익을 관철하지 못한 잘못. 고스란히 당신이 감내해야 한다.

이제 대선이 남았다. 아마도 대선 전에 로또가 된다면 그 땐 내가 지지하는 정당이 바뀌어 있을 것이다. 만약 그런 날이 온다면 그때는 나를 비난해주기 바란다. 왜냐면 당신들이 그런 나를 비난할 때 나는 내 이익을 관철시키기가 한결 수월해질 것이기 때문에, 그런 비난의 글들을 흐뭇하게 바라볼 수 있을 것 같다.

 

[iOS] UIManagedDocument로 만드는 코어데이터 메모장

빈 프로젝트에서 코어데이터를 시작하는 방법

일반적으로 코어데이터 기반의 앱을 만들 때는 프로젝트 생성시부터 코어데이터를 사용하도록 지정하여 코어데이터 사용에 필요한 기본적인 코드들이 미리 생성되도록 하여 사용한다. 하지만 불행히도 Xcode의 모든 템플릿이 코어데이터를 지원하지는 않는다. (예를 들면 현재 4.2 버전에서 싱글 뷰 기반 앱이 그러하다. )

이 번에는 예전에 만들어 본 적 있는 코어데이터 메모장 프로젝트를 완전히 새로 만들면서 (사실 그 당시 작성한 내용은 그저 “저장을 어떻게 하는가”를 보여주기 위한 내용이었으므로) 코어데이터 기반으로 시작하지 않은 프로젝트에서 코어데이터를 추가하는 방법과, 여러 화면 사이를 오갈 때 데이터를 전달하는 방법 (이전에는 무리하게 앱 델리게이트에 객체를 맡겨두었는데 상당히 좋지 않은 스타일이다)에 대해 알아보도록 하겠다.

UIDocument

iOS에서도 OSX처럼 문서 기반의 앱을 만드는 것이 수월하게 가능하도록 하는 UIDocument 객체가 있다. 이 객체의 서브클래스로 UIManagedDocument가 있는데, 이는 코어데이터와 통합된 문서 객체를 만드는 것을 지원한다. 즉 문서 파일 (정확히는 파일 래퍼[file wrapper]이다) 내에 코어데이터 데이터베이스 파일을 보관하는 것이다. 즉 코어데이터 템플릿이 아닌 프로젝트에서 코어데이터를 사용하고자 한다면 UIManagedDocument 객체를 생성하면 된다.

코어데이터 프레임워크에서 핵심이 되는 객체는 managedObjectContext 인데, 이 컨텍스트를획득하기 위해서는 로컬이든 iCloud이든 어딘가에 실제로 저장된 파일이 필요하다. 코어데이터 템플릿은 이 파일을 자동으로 생성하고 이 파일에 대해 영구저장소 코디네이터를 만들어 컨텍스트를 생성해주는 코드를 포함하고 있다. 같은 방식으로 NSManagedDocument 객체를 생성하면 파일에 대한 코디네이터 및 컨텍스트가 자동으로 생성되므로 손쉽게 코어데이터를 사용해 사용자가 작성한 내용을 영구적으로 저장할 수 있다.

더군다나 UIDocument 객체는 변경 사항을 자동으로 저장할 수 있고, 아이튠즈에서도 이 문서 객체를 데스크탑으로 옮길 수도 있다. 또한 iCloud와의 연동에 대한 부분도 쉽게 할 수 있어 이를 통해 정말 쿨하게 코어데이터를 쉽게 사용할 수 있다.

프로젝트 생성

적당한 이름을 주고 프로젝트를 생성한다. 시작은 비어있는 프로젝트 템플릿이다. 코어데이터 사용 여부에는 따로 체크하지 않는다. 대신 프로젝트를 생성한 다음 코어데이터 프레임워크를 추가해준다.

메모 모델

메모 데이터 모델은 간단히 다음 4개 속성을 가지게 될 것이다. 데이터 모델을 나중에 만들게 될 것이므로 우선 다음의 이름과 유형으로 만들어질 것이라고 알아둔다.

  • 제목 (문자열) – title
  • 내용 (문자열) – content
  • 생성일 (날짜시간) – createdDate
  • 최종수정일 (날짜시간) – lastModifiedDate

앱 델리게이트

메모장 앱은 실행되면서 저장된 내용을 읽어와야 한다. 이 작업은 앱 델리게이트에서 담당하도록 한다. 따라서 앱 델리게이트에서 문서 객체를 만들어 다른 객체가 이 문서의 managedObjectContext를 참조하도록 하면 된다.

앱 델리게이트의 인터페이스 파일에서는 코어데이터 프레임워크를 <CoreData/CoreData.h> 를 임포트하여 반입하고, 다음과 같은 프로퍼티를 지정해준다.

@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;

이제 구현부 파일에서 문서를 만들도록 하자. private 인터페이스에서 문서객체를 프로퍼티로 선언한다.

@interface AppDelegate()
@property (strong, nonatomic) NSManagedDocument *storedDocument
@end

 

앱이 실행되면 문서 객체를 생성한다.

@synthesize storedDocument = _storedDocument;

문서 객체의 생성과 문서 열기

이제 실행 완료시 호출되는 -applicationDidFinishLaunchingWithOptionos: 를 구현한다. 이 때 몇 가지 유의할 점이 있다.

  • UIDocument는 파일 패스가 아닌 URL 기반으로 생성해야 한다.
  • 문서 파일이 있다면 파일을 열어야 그 속에 있는 코어데이터 파일을 사용할 수 있고
  • 문서 파일이 없다면 초기 저장을 통해 문서와 파일들을 생성해 주어야 한다.
  • 또한 문서를 열거나 저장하는 작업은 비동기적으로 처리되므로 이에 주의해야 한다.

그럼 문서 객체를 생성해보자.

-(BOOL) applicationDidFinishLaunchingWithOptions:(NSDictionary *)options
{
  NSString *docFilename = @"doc.data";
  NSString *docPath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES) objectAtIndex:0]
                       stringByAppendingPathComponent:docFilename];
  NSURL *pathURL = [NSURL fileURLWithPath:docPath];

  _storedDocument = [[UIManagedDocument alloc] initWithFileURL:urlPath];
// 파일이 있는지 검사
  if ([[NSFileManage defaultManage] fileExistsAtPath:docPath]) {
// 파일이 있다면 열어야 한다.
    [_storedDocument openWithCompletionHandler:^(BOOL success){
      if(!success) {
        NSLog(@"fail to open document");
      } else {
        self.managedObjectContext = _storedDocument.managedObjectContext;
        // 추가적인 액션이 필요하다.
      }
        }];
    }
    else {
      // 파일이 없다면 저장을 해야한다.
      [_storedDocument saveToURL:pathURL forSaveOperation:UIDocumentSaveForCreating
               completionHandler:^(BOOL success){
        if(!success) {
          NSLog(@"fail to create document");
        } else {
          self.managedObjectContext = _storedDocument.managedObjectContext;
          // 추가적인 액션이 필요하다.
        }
      }];
    }
  return YES;
}

 

문서을 열거나 저장할 때는 -openWithCompletionHandler:-saveToURL:forSaveOperation: 메소드를 사용하는데, 두 메소드는 모두 코드블럭을 인자로 넘겨주고 있다. 즉 문서를 열고 저장하는 작업은 로컬에서는 즉각적일 수 있으나, icloud와 연계된 경우라면 시간이 걸릴 수 있는 비동기적인 작업인 것이다. 따라서 외부의 객체 (이 경우에는 루트 뷰 컨트롤러가 될 것이다.)는 문서의 저장이나 로딩이 완료된 이후에 문서의 managedObjectContext를 획득해 가야 한다.따라서 추가적인 액션이 필요하다고한 부분에 대해서는 바로 이 부분에 대해 이야기한 것이다.

문서가 열리는 시점에 대한 고민

문서는 문서 객체를 만들고 나서 “열어야” 사용할 수 있다고 하였다. 그리고 이 문서를 여는 작업은 스토리지를 액세스해야 하므로 약간의 지연이 발생할 수 있다. 따라서 메모의 목록을 표시하는 루트 뷰 컨트롤러가 적절한 시점에 컨텍스트를 요청해야 유효한 컨텍스트를 얻어갈 수 있다는 부분이 요점이 된다.

이 부분을 어떻게 처리하면 좋을까? 바로 델리게이션이다. 즉 루트 뷰 컨트롤러가 이 앱델리게이트의 델리게이트가 되고, 앱 델리게이트에서는 문서 로딩이 완료되면 델리게이트(루트 뷰 컨트롤러)에게 managedContextObject를 전달해주면 되는 것이다.

이를 구현하기 위해서는 두 부분이 필요하다. 첫째로 델리게이트 객체를 만드는 것이고, 둘째로 이 델리게이트가 구현해야할 메소드를 지정하는 것이다. 구현은 델리게이트 측에서 하게 될 것이므로 여기서는 선언만 하면 된다. 그렇다 그게 바로 프로토콜이다.

앱델리게이트의 델리게이트 만들기

인터페이스 파일에서 프로토콜과 델리게이트를 선언해준다. 프로토콜은 @interface 블럭 밖에서 선언해야 한다.

@protocol ManagedDocumentDelegate <NSObject>
-(void)managedDocumentDidFinishOpening:(id)sender;
@end

 

이제 델리게이트를 선언해준다. 델리게이트가 될 객체의 클래스는 특정하기가 어려우므로 id를 사용한다. 특히 델리게이트는 위에서 선언한 프로토콜을 준수해야 하므로, 이를 명시적으로 표현하기 위해서는 다음과 같이 코딩한다.

@propery (strong, nonatomic) id<ManagedDocuementDelegate> delegate;

다시 구현부 파일에서는 synth한다.

@synthesize delegate = _delegate;

다시 문서 열기 및 저장과 관련한 부분에서 그 ‘추가적인 액션’은 델리게이트에게 자신을 넘겨줌으로써 델리게이트가 managedObjectContext를 획득하도록 하는 부분인 것이다.

[self.delegate managedDocumentDidFinishOpening:self];

이 코드를 추가적인 액션… 부분에 각각 추가해주도록 한다.

루트 뷰 컨트롤러

이제 앱델리게이트가 할 일은 모두 끝났다. 정말이다. 이게 전부이다. 오히려 코어데이터 기반 프로젝트로 시작할 때보다 훨씬 더 깔끔하게 끝낸 기분이 들지 않는가?

이제 UI를 생성하기 전에 먼저 루트 뷰 컨트롤러의 클래스를 작성하자. 루트 뷰 컨트롤러는 위에서 우리가 정한 ManagedDocumentDelegate  프로토콜을 따라야한다. 새 파일을 생성한다. UIViewController의 서브 클래스로 파일을 생성하고, 구현부 파일을 연다.

private interface에서는 ManagedDocumentDelegate 프로토콜을 따른다고 명시한다. 또한 NSManagedObjectContext와 NSMutableArray 하나를 각각 프로퍼티로 선언한다.

#import "AppDelegate.h"
@interface RootViewController() <MemoDocumentDelegate>
@property (strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (strong, nonatomic) NSMutableArray *memoListArray;
@end

@implemtation RootViewController
@synthesize managedObjectContext = _managedObjectContext;
@synthesize memoListArray = _memoListArray;
...

특히, NSMutableArray의 경우에는 초기화하는 코드가 없으면 나중에 객체를 추가하거나 뺄 때 에러를 뿜으며 종료된다. -viewDidLoad 에서 초기화할 수도 있지만 실제로 필요한 시점에 초기화되도록하려면 다음과 같이 접근자 메서드를 구현한다.

-(NSMuatbleArray *)memoListArray
{
  if(!_memoListArray) _memoListArray = [NSMutableArray array];
  return _memoListArray;
}

 

대신 -viewDidLoad에서는 자신을 앱델리게이트 객체의 델리게이트로 지정해야 한다. 그래야 문서가 열릴 때 이 내용을 전달 받을 수 있다.

-(void)viewDidLoad
{
  [super viewDidLoad];
  [[[UIApplication sharedApplication] delegate] setDelegate:self];
}

 

이제 앱 델리게이트가 managedContext를 생성하고 포인터를 얻었을 때 호출해 줄 메서드를 구현한다.

-(void)managedDocumentDidFinishOpening:(id)sender
{
  self.managedObjectContext = sender.managedObjectContext;
  // 목록을 갱신해 준다.
}

 

문서가 열리고 나면 컨텍스트를 획득하게 되니, 그 이후에 목록을 갱신해주면 된다.

이 부분까지는 사실 앱이 시작되는 시점에 일어날 일들을 처리해준 것에 지나지 않는다. 이후 루트 뷰 컨트롤러는 다음의 기능을 처리해야 한다. (그렇다. 갈길이 멀다.)

  • 코어데이터로부터 저장된 메모들을 불러오는 작업
  • 불러온 데이터를 뷰 컨트롤러 내 테이블 뷰에 보여주는 작업
  • (화면 어딘가에 있을) 신규 작성 버튼을 눌러 메모 작성 화면을 호출하는 기능
  • 메모 작성이 완료될 때 이를 저장하는 기능
  • 기존 메모를 선택할 때 그 상세 내용을 보여주는 화면을 호출하는 기능
  • 메모 목록에서 메모를 삭제하는 기능

메모 작성 화면 생성

사실 이 시점에서는 스토리보드를 짓기 시작하면 좋지만, 귀찮은 기분이들어서 그냥 디테일 뷰 컨트롤러를 작성하는 것부터 시작하도록 한다. 또 하나의 뷰 컨트롤러 클래스를 하나 생성한다. 이름은 DetailViewController가 적당하겠다. 이 컨트롤러는 다음과 같은 기능을 수행한다.

  • 신규 메모를 작성
  • 기존 메모의 상세 내용을 표시
  • 기존 메모를 편집

따라서 이 컨트롤러는 다음과 같은 속성들을 가지고 있어야 한다.

  • 뷰가 호출될 때 신규메모를 작성하는지, 기존 메모를 표시할 것인지를 저장할 프로퍼티
  • 편집이 완료되었을 때 상위 뷰 컨트롤러 (presentingViewController)에게 이를 알려주는 기능

그런데 우리는 두 번째 기능을 앞서 살펴본 델리게이션을 통해 구현해보고자 한다. 또한 신규 작성시에는 Modal 의 형태로 표시되고, 기존 메모를 선택한 경우에는 네비게이션 스택에 뷰가 추가되도록 할 것이다.(후자가 기존에 사용한 방법이다.)

인터페이스 파일

따라서 인터페이스 파일에서는 메모 작성이 완료되었을 때 델리게이트가 처리해줄 메서드를 정의하고, 두 개의 프로퍼티 (현재 편집할 메모 객체/ 델리게이트) 를 추가해준다.그 외 이 클래스가 사용하는 모든 프로퍼티는 외부에서 참조할 필요가 없으므로 private interface에 선언할 것이다.

@protocol MemoDetailDelegate
-(void)memoDetailDidFinishEditingMemo:(id)memo isNew:(BOOL)new;
@end

@interface DetailViewController : UIViewController
@property (strong, nonatomic) id currentMemo;
@property (weak, nonatomic) id<MemoDetailDelegae> delegate;
@end

 

프로토콜에서 선언한 메소드는 현재 편집하던 메모의 편집이 끝났다면서 그 메모를 델리게이트에게 전달해주고, isNew: 파라미터를 통해 신규 메모가 추가되었던 것인지, 기존 메모를 편집하던 것인지를 함께 알려준다. 또한 아직 코어데이터 모델 및 모델객체 클래스를 만들지 않아 이들을 id 타입으로 선언해주었다. (이는 나중에 추가해주어야 한다.)

내부 인터페이스

이제 구현부 파일에서 내부 인터페이스를 선언한다. 내부 인터페이스에서는 다음과 같은 프로퍼티를 선언해준다.

  • 화면에 표시될 제목 (텍스트필드)
  • 메모 내용을 담을 텍스트뷰 (텍스트뷰)
  • 현재 편집하는 메모가 신규 메모인지 여부를 기억하는 플래그
@interface DetailViewController()
@property (weak, nonatomic) IBOutlet UITextField *titleField;
@property (weak, nonatomic) IBOutlet UITextView *contentView;
@property (assign) BOOL isAdding;
@end

 

외부 인터페이스 및 내부 인터페이스에서 선언한 프로퍼티를 synth 해준다.

@synthesize currentMemo = _currentMemo, delegate = _delegate;
@synthesize titleField = _titleField, contentView = _contentView, isAdding = _isAdding

 

디테일 뷰가 열리기 전에 루트 뷰에서는 디테일뷰에서 작성할 메모 객체를 전달해 줄 것이다. 그 때 신규 메모 여부를 판별하기로 한다.

-(void)setCurrentMemo:(id)currentMemo
{
  _currentMemo = currentMemo;
  if(![_currentMemo valueForKey:@"lastModifiedDate"]) {
    // 최종 수정일이 nil 이라는 것은 신규 메모를 의미한다.
    self.isAdding = YES;
  }
}

 

또한 뷰가 화면에 표시되기 전에 기존 메모인 경우에는 제목과 내용을 미리 표시해주도록 한다.

-(void)viewWillAppear
{
  if(!self.isAdding) {
    self.titleField.text = [self.currentMemo valueForKey:@"title"];
    self.contentView.text = [self.currentMemo valueForKey:@"content"];
  }
}

 

또한 저장 버튼을 눌렀을 때는 델리게이트(아마도 루트 뷰 컨트롤러)에게 다음의 적절한 동작을 하도록 시킬 수 있을 것이다.

-(IBAction)savePressed
{
  [self.currentMemo setValue:self.titleField.text forKey:@"title"];
  [self.currentMemo setValue:self.contentView.text forKey:@"content"];
  if(self.isAdding) [self.currentMemo setValue:[NSDate date] forKey:@"createdDate"];
  [self.currentMemo setValue:[NSDate date] forKey:@"lastModifiedDate"];

  [self.delegate memoDetailDidFinishEditingMemo:self.currentMemo isNew:self.isAdding];
}

 

이제 다시 남은 작업은 루트 뷰 컨트롤러의 몫이 되었다. 즉 디테일 뷰 컨트롤러의 델리게이트(루트 뷰 컨트롤러)가 최종적으로 남은 작업을 수행한다. 델리게이트는 신규 메모를 받아가므로 다음 작업을 처리해야 한다.

  • 신규 메모인 경우 이 메모를 메모 리스트 배열에 추가한다.
  • 테이블 뷰를 갱신한다.

다시 루트 뷰 컨트롤러

다시 RootViweController.m 에서 남은 작업을 처리하는 코드를 입력한다.

@interface RootViewController () <MemoDocumentDelegate, MemoDetailDelegate>

로 디테일 뷰 컨트롤러의 프로토콜을 따른다고 수정해준 다음, 프로토콜이 선언한 메소드를 구현한다.

-(void)memoDetailDidFinishEditingMemo:(id)Memo isNew:(BOOL)isNew {
  if (isNew) [self.memoListArray insertObject:memo atIndex:0]; 
  [self dissmissModalViewControllerAnimated:YES];
  // 테이블 내용을 갱신해줌
}

 

이제는 실제 데이터 모델과 UI를 만들고 나서 테이블 뷰에 실제 메모 데이터를 남기도록 하는 부분의 작업을 이어나가도록 한다.

모델 만들기

신규 파일을 작성한다. 파일의 종류는 코어데이터 모델이다. 코어 데이터 튜토리얼 등에서 흔히 볼 수 있는 엔티티 편집기에서 새 엔티티를 만든다. 엔티티의 이름은 Memo로 한다. 엔티티는 DB의 테이블에 해당한다고 보면된다. 엔티티의 이름은 클래스의 이름이 되므로 대문자로 시작하는 것이 관례이다. Memo 엔티티를 추가했으면, 거기다가 4개의 속성(attribute)들을 추가한다. 그리고 글의 서두에서 말한 것처럼 각각의 속성의 이름과 타입을 지정한다. 속성은 DB에서는 각각의 필드이며, 클래스의 프로퍼티에 해당하므로 소문자로 시작하는 이름을 따른다.

속성 추가를 완료하면 메뉴에서 Editor > Create ManagedObjectModel Subclass… 를 선택해서 해당 엔티티의 모델 클래스(Memo)를 생성한다.

UI 만들기

이번에는 스토리보드 파일을 추가한다. (스토리보드 파일 추가 후에는 꼭 타겟 속성에서 스토리보드 이름을 기입해주도록 하자)

  1. 뷰 컨트롤러를 하나 추가한다. 클래스 이름은 RootViewController. editor 메뉴에서 embed > Navigatoin Controller를 선택해 네비게이션 컨트롤러를 추가한다.
  2. 루트 뷰 컨트롤러에 상단 네비게이션 막대가 생기는데, BarButtonItem을 선택해 오른쪽에 추가한다. 추가한 버튼의 유형은 Compose로 만든다.
  3. 아직 관련된 소스는 입력하지 않았지만 테이블 뷰도 하나 올려준다. (테이블 뷰 컨트롤러가 아님) 테이블 뷰를 올렸으면 테이블 뷰를 우클릭으로 끌어 하단의 검은 막대에 있는 뷰 컨트롤러 클래스 아이콘으로 연결한다. datasource와 delegate를 각각 연결해준다. 즉, 두 번 연결해야 한다.
  4. 새 뷰 컨트롤러를 하나 추가한다. 클래스 이름은 DetailViewController로 변경해준다.
  5. 디테일 뷰 컨트롤러에는 텍스트 필드 하나, 텍스트 뷰 하나, 버튼 하나를 올려준다. 그리고 두 입력 필드는 미리 지정해놓은 아울렛과 연결한다. 어시스턴트 에디터를 열고 소스코드를 구현부 파일로 선택해서 save 버튼을 작성해 놓은 액션에 우클릭으로 끌면 (컨트롤+드래드) 해당 메소드와 연결할 수도 있다.
  6. 루트뷰 컨트롤러의 상단 ‘작성’ 버튼을 우클릭하여 디테일 뷰로 끌어준다. segue가 생성되는데, 이 segue를 클릭하여 선택하고 속성창에서 identifier 란에 AddMemoSegue라고 입력해준다. 이는 나중에 사용될 값이므로 복사해두자.

남은 작업

실행해보기 전에 한가지 고려할 것이 있다. 앱을 실행하면 바로 리스트 화면 (테이블 뷰)가 표시된다. 동시에 앱 델리게이트는 문서 파일을 생성하고 이를 열어 컨텍스트를 생성한다. 이 작업이 완료되면 다시 루트 뷰 컨트롤러에 이 사실을 알려준다. (델리게이션) 이 시점 전까지는 루트 뷰 컨트롤러는 컨텍스트를 가지고 있지 않으므로 새 메모를 작성할 수 없어야 한다. 따라서 이 부분에 대한 수정이 필요하다.

테이블 뷰에 데이터 표시하기

또한 컨텍스트를 획득하고 나면 저장된 메모들을 불러와 화면에 표시하는 작업을 해 줘야 한다. 이 때 강제로 테이블 뷰를 갱신하므로 테이블뷰에 대한 아울렛도 하나 필요하다.

루트 뷰 컨트롤러에 아울렛을 하나 추가한다.

@property (weak, nonatomic) IBOutlet UITableView *listTableView;

를 내부 인터페이스에 추가한 다음, 스토리보드에서 테이블 뷰 아울렛을 컨트롤러에 연결해준다. (이 연결해주는 작업을 하지 않으면… 갱신이 안된다. 흔히 하는 실수이므로 꼭 습관을 들여야 한다.)

먼저 저장된 내용을 가져오는 메소드를 작성해보자. 이제 코어데이터의 기능을 본격적으로 사용해야하므로 파일의 머리 부분에 코어데이터 프레임워크를 임포트하는 구문도 하나 추가해주어야 한다. 또한 이 루트 뷰 컨트롤러가 테이블 뷰의 두 프로토콜을 따르는 부분이 있으므로 이에 관한 내용을 내부 인터페이스에 추가해주자.

#import <CoreData/CoreData.h>

@interface RootViewController () <MemoDocumentDelegate, MemoDetailDelegate, UITableViewDatasource, UITableViewDelegate >

 

다음은 코어데이터에서 내용을 가져오는 부분이다.

-(NSArray *)fetchData
{
  NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
  NSEntityDescroption *anEntity = [NSEntityDescription entityForName:@"Memo" inManagedObjectContext:self.managedObjectContext];
  NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"lastModifiedDate" ascending:YES];

  fetchRequest.entity = anEntity;
  fetchRequest.sortDescriptors = [NSArray arrayWithObject:sortDescriptor];

  NSArray *result = [self.managedObjectContext excuteFetchRequest:fetchRequest error:nil];
}

 

이제 테이블 뷰의 데이터 소스 부분이다. 이 부분은 이전의 내용과 완전히 동일하다.

-(NSInteger)tableview:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
  return [self.memoListArray count];
}

-(UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  NSString *cellIdentifier = @"MemoListCell";
  UITableViewCell *cell = [tableView dequeReusableCellWithIdentifier:cellIdentifier];
  cell.textLabel.text = [[self.memoListArray objectAtIndex:indexPath.row] valueForKey:@"title"];
  NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  dateFormatter.timeStyle = NSDateFormatterMediumStyle;
  dateFormatter.dateStyle = NSDateFormatterMediumStyle;
  NSString *dateString = [dateFormatter stringFromDate:(NSDate*)[[self.memoListArray objectAtIndex:indexPath.row] valueForKey:@"lastModifiedDate"]];
  cell.detailTextLabel.text = dateString;

  return cell;
}

 

주의할 부분

앱을 빌드하고 실행하기 전에 루트 뷰 컨트롤러에서 신규 메모 작성 버튼이 컨텍스트가 존재할 때만 활성화되도록 변경하여 처리해준다. 이를 위해서는 툴바 버튼에 대한 아울렛을 하나 생성하고 초기화 시와 컨텍스트 획득시에 각각 코드를 처리한다.

@property (weak, nonatomic) IBOutlet UIBarButtonItem *composeButton;

synth 코드를 추가한 다음, 아래 코드를 -viewDidLoad 에 추가한다.

self.composeButton.enabled = NO;

컨텍스트를 확인하는 시점은 -managedDocumentDidFinishOpening: 이다. 마지막에 아래 코드를 추가한다.

if(self.managedObjectContext) self.composeButton.enabled = YES;

신규 메모를 작성하기 전에 처리할 내용

이제 실제로 디테일 뷰를 불러오기 전에 디테일 뷰에 신규 생성된 메모의 포인터를 넘겨주는 부분을 처리하자. 우리는 화면 전환을 segue로 하고 있으므로, segue가 이를 처리하면 된다. segue가 동작하기 전에는 -prepareForSegue: sender: 를 호출하는데 여기서 처리해주면 된다.

-(void)prepareForSeuge:(UIStoryboradSegue *)segue sender:(id)sender
{
  if([segue.identifier isEqualToString:@"AddMemoSegue"]) {
    DetailViewController *editor = segue.destinationViewController;
    Memo *newMemo = [NSEntityDescription insertNewObjectForEntityForName:"@Memo" inManagedObjectContext:self.managedObjectContext];
    editor.currentMemo = newMemo;
    editor.delegate = self;
  }
}

끝으로 메모 목록이 업데이트될 때 테이블뷰를 갱신하는 코드를 추가한다. 주석으로 목록을 갱신한다고 표시한 부분에 [self.listTableView reloadData]; 를 추가해준다. (두 델리게이트 메소드 부분에 있다.)

기존 메모 편집하기

이제 앱을 빌드하고 실행하면 기본적으로 메모를 작성하고 이 메모가 테이블에 추가되는 동작을 확인할 수 있다. (물론 아직 기존 내용을 확인하는 내용은 구현하지 않았다.)

주목할 부분은 어디에도 컨텍스트를 저장하는 부분이 없다는 것이다. 데이터의 저장은 UIManagedDocument가 알아서 처리하므로 따로 저장하는 액션을 취해줄 필요는 없다.

저장된 메모를 편집하기 위해서는 테이블 뷰 셀에서 디테일 뷰 컨트롤러로 segue를 추가한다음, prepareForSegue: 에서 해당 segue 일 때의 액션에서 디테일뷰에 현재 선택된 인덱스의 메모를 전달해주면 된다. 기존 메모를 편집하고 저장했을 때 변경 사항을 어떻게 처리하는가? 이 부분은 이미 다 처리되어 있다 (읭?)

스토리보드에서 테이블 뷰 셀에서 디테일 뷰 컨트롤러로 segue를 추가한다음, ViewMemoSegue라는 이름을 준다. 다음 루트 뷰에서 아래 코드를 추가하자.

-(void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
....
  else if ([segue.identifier isEqualToString:@"ViewMemoSegue"]) {
    DetailViewController *editor = segue.destinationViewController;
    editor.currentMemo = [self.memoListArray objectAtIndex:self.listTableView.indexPathOfSelectedRow.row];
    editor.delegate = self;
  }
}

메모를 삭제하는 코드는 역시 지난 시간에 만든 내용 똑같이 사용하면 되므로 기존 글을 참고하도록 한다.

20111022 :: 이를테면 SWOT 분석 같은 것

개인적으로 ‘경영학’을 그다지 좋아하지 않는다. 수 많은 사람들이 경영학을 전공하고 심지어는 복수 전공으로 경영학을 택하는 공대생, 자연대생, 생활대생 들이 있지만 나는 경영학을 정말이지 싫어한다. 그 저변에는 내 스스로가 경제나 경영에서부터 회계에 이르기까지 숫자를 이용하면서도 애매모호하기 그지 없는 여러 개념들 때문이기도 하지만 가장 큰 이유는 “경영학과생”들 때문이다. 보다 정확히는 “경영학과생들의 발표 스타일” 때문이겠지.

군대를 마치고 복학했던 2000년대 초중반의 학교는 사뭇 분위기가 많이 달라져 있었다. 대부분의 수업은 학생들의 발표로 채워지는 경향이 강했고, 그리고 대부분의 발표는 당시 경영학과의 인기를 방증하듯 경영대 스타일의 자료가 넘쳐났다. 문제는 그 발표자료들이나 발표의 스타일이 하나 같이 똑같은 템플릿이었다는 것과, 보고서와 프리젠테이션을 구분하기 힘들 정도의 엄청난 텍스트를 포함하고 있었다는 것이었다. 당연히 발표자는 그 텍스트를 줄줄 읽어내려가는 스타일의 발표들을 했고, 무엇보다 글을 읽을 줄 아는 사람이 눈으로 글을 읽어 나가는 속도는 말로 글을 읽어주는 속도보다 월등하기에 발표가 귀에 들어올리 만무했다는 것이다.

가장 못마땅하게 생각되는 자료의 유형 중의 하나는 SWOT 분석이다. 경영대 생들은 정말 이걸 좋아하는 것 같았다. 거의 모든 과목에서 저 도표를 보았던 것 같다. 문제는 그게 얼마나 쓸모 없는 건가 하는 생각이 발표 내내 아니 학교를 다니던 내내 들었다라는 점이다. SWOT 분석은 보통 종이에 큰 십자선을 긋고 나누어진 4개의 영역에 강점/약점/기회/위기의 요소를 기재하여 쓴다. 그런데 이 도표를 만드는 과정을 생각해보면 그저 헛웃음만 나올 뿐이다. 자기 스스로의 강점이 분명한 경우에는 저렇게 쓸 필요도 없겠지만 보통은 스스로의 강점과 약점을 일목요연하게 말하기는 힘들다. 시장에서의 위기나 기회같은 요소도 어찌보면 비슷한 맥락이다. 즉, 저 한 장의 도표에 있는 10~16개 가량의 문장을 쓰기 위해 발표자는 상당한 시간을 들였을지는 몰라도, 만약 진짜 SWOT가 중요한 어떤 프로젝트라면 그건 굳이 발표자료에 포함시키지 않더라도 발표자 뿐만 아니라 프로젝트에 참가한 모든 팀원 나아가서 아마 그 발표를 듣고 있는 사람에게까지도 중요해서 “모두 다 아는 이야기”라는 점이다.

결국 그닥 쓸모 없는 페이지를 양산하기 위해서 그렇게 많은 시간을 들일 필요가 있을까. 차라리 보다 더 명확한 메시지를 정리하기 위해 시간을 할애했으면 좋지는 않았을까 하는 생각이 든다. 졸업 이후에 일을 시작하면서도 이런 저런 프로젝트나 TF 등에서도 가끔 저 SWOT 분석을 볼 일이 있었는데, 그럴 때마다 회의실을 뛰쳐나가고 싶은 욕망이 한 순간 온몸을 감싸는 듯한 기분을 느끼기도 했다.

사실 나는 발표를 그닥 잘 하지 못한다. 좀 작은 목소리와 약간 웅얼거리는 듯한 말투, 불분명한 발음 같은 것들 때문에 여러 사람 앞에서 말하는 게 그다지 자신 있거나 하지는 않다. 그럼에도 불구하고 당신이 어떤 발표를 앞둔 상황이라면 메시지를 최대한 단순하게 하라는 말은 해 줄 수 있다. 메시지를 단순화하면 그다지 많은 글을 쓸 수가 없고, 그러면 슬라이드보다 당신이 할 수 있는 말이 많아진다. 물론 그러려면 슬라이드를 만드는 작업보다는 ‘해야 할 말’에 대해 깊은 생각을 더 많이 해야해서 힘들지는 모르지만 최소한 따분한 발표는 하지 않게 된다. 보통은 따분한 발표는 그 자리에 참석한 많은 사람들의 시간만 좀먹는 현대 사회의 적이 될 수 있으므로 이렇게 함으로써 최소한, “예의 바른” 발표자는 될 수 있을 것이다.