Archive

Posts Tagged ‘아이폰’

리피 제작 의도 – 채널이란 무엇인가?

8월 11th, 2010

정말 오래간만에 블로깅합니다. 트위터나 미투데이를 하면서 블로그에 소홀해진 점도 있지만 지난 4월부터 저희 회사에서 만든 리피(liipii)라는 서비스 개발에 집중하다보니 블로그에 글을 적을 시간이 부족했습니다. 사실 리피를 개발하면서 아이폰 및 레일스 관련 글을 몇개 적긴했지만 내용도 부족하고 정리도 안된 상태라 임시 저장 상태로 봉인해버렸습니다 ;;

리피는 채널(channel) 중심의 소셜 서비스(SNS)입니다. 기존의 트위터나 미투데이와 같은 소셜 서비스들이 개인 중심이라면 리피는 채널 중심입니다. 현재 리피는 아이폰 앱으로 먼저 출시되었으며 빠른 시일 내에 웹 및 모바일 웹 버전을 출시할 예정입니다.

요즘은 많은 분들이 트위터나 미투데이를 사용하지만 아직까지 트위터나 미투데이를 어려워 하시는 분들도 있습니다. 트위터나 미투데이의 가장 큰 단점은 처음 가입했을 때 별로 할 일이 없다는 점입니다. 구독자(follower)나 친구가 없으니 당연히 혼자 놀기를 할 수밖에 없습니다. 많은 경우 혼자 놀다 흥미를 잃어버리곤 합니다. 그러나 소셜시대라는 대세를 거스를 수 없다면 팔로우(follow) 또는 친구 신청 열심히 해야 합니다. 모든 일은 비용이 들기 마련이지만 당장 누군가와 대화하고 싶은데 그럴 수 없다는 것은 매우 안타까운 상황입니다. 바로 이 점이 처음 리피를 기획하게 된 이유였습니다.

만들지 얼마되지 않아 썰렁한 리피 트위터 만들지 얼마되지 않아 썰렁한 리피 미투데이

처음 만나는 사람들이 쉽게 친해질 수 있는 방법은 공통의 관심사를 찾는 것입니다. 우리는 흔히 모르는 사람(이성이든 동성이든)을 처음 만난 경우 이것저것 내용을 바꿔어 가면서 대화를 하다가 공통으로 좋아하는 주제가 나오면 해당 이야기를 이어가면서 서로 동질감을 느끼고 호감으로 발전해갑니다. 이런 패턴을 관찰하면서 채널을 이란 개념을 만들었습니다. 채널이란 특정 주제에 대해서 대화할 수 있는 공간으로 채팅의 채팅방에서 그 모티브를 가져왔습니다. 리피 안에 다양한 주제의 채널이 열려있고 누구든지 그리고 언제든지 관심이 가는 채널만 찾아 들어가면 공통의 관심사를 가진 사람과 바로 대화할 수 있을 것이라고 생각했습니다.

채팅은 실시간 대화인 반면에 트위터나 미투데이와 같은 소셜 서비스는 실시간이라고 보기는 어렵습니다. 물론 블로그와 같은 서비스와 비교해서 실시간 성이 강하며 스마트 폰의 앱들은 알림(push) 기능을 지원하기 때문에 거의 실시간에 가깝다고 볼 수 있습니다. 실시간 성이란 결국 속도감을 의미하며 속도감은 트위터와 같은 단문 대화 서비스의 핵심적인 요소입니다. 그럼에도 불구하고 트위터나 미투데이는 정확한 의미에서 실시간 서비스가 아닙니다. 그리고 실시간이라고 무조건 좋은 것은 아닙니다. 실시간이라는 것은 많은 집중을 요구하기 때문에 그만큼 사용 부담이 크다는 것을 의미하기도 합니다. 사용 부담이 크다는 것은 결과적으로 사용 빈도가 떨어뜨리는 요인이 됩니다.

아무튼 채팅의 채널이라는 개념을 소셜 서비스에 맞게 바꿀 필요가 있었습니다. 우선 채팅의 입장(join)이라는 개념을 빼버렸습니다. 채널이라는 것이 앱 또는 웹 상에 완전히 열려 있기 때문에 폐쇄적이고 번거로운 입장이라는 개념은 필요하지 않았습니다. 대신 구독(follow) 개념을 추가해서 관심 있는 채널의 대화 내용을 모아볼 수 있도록 기획하였습니다. 마찬가지로 채널이 열려 있기 때문에 누구든지 대화를 올릴 수 있도록 했습니다. 극단적인 경우 대화를 올리지만 구독은 하지 않는 경우도 허용하였습니다.

리피 채널 목록 나의 리피 (구독 채널 모아보기)

그리고 채널에 유효 시간이라는 개념을 추가하였습니다. 채팅의 채널은 마지작 참여자가 퇴장(quit)하는 순간 채널이 종료됩니다. 하지만 리피에는 입장이라는 개념이 없기 때문에 퇴장이라는 개념 또한 없으며 결국 채널은 계속 지속될 수 밖에 없습니다. 채널이 계속 지속되는 경우 실시간 성이 떨어지고 결국 속도감도 떨어질 수 밖에 없다고 판단하였습니다. 그래서 채널을 만든 시점부터 24시간 동안만 대화를 올릴 수 있도록 기획했습니다. 24시간이 지나면 더이상 대화를 올릴 수 없기 때문에 최신 대화 순으로 보여지는 타임라인 상에서 해당 채널의 대화는 더이상 노출되지 않게 됩니다. 물론 채널은 열려있기 때문에 언제든지 이전 채널을 찾아들어갈 수 있습니다.

지금까지 채널의 제작 의도에 대해서 정리해보았습니다. 개발하면서 이것거것 고민을 많이 했기 때문에 할 말은 많지만 글이 길어지면 적는 사람도 읽는 사람도 힘들어지기 때문에 이쯤에서 마무리하려 합니다. 아직 못다한 얘기는 다른 주제로 다시 포스팅하도록 하겠습니다.

이 글을 읽는 동안 리피에 대해서 관심이 생기셨다면 꼭! 리피를 써보시기 바랍니다. 리피는 아이폰 앱스토어에서 다운받으실 수 있습니다. 만약 아이폰이 없으시다면 조금만 기다려 주시기 바랍니다. 가능한 빨리 웹 및 모바일 웹 버전을 출시하도록 하겠습니다.

더불어 리피 아이폰 앱 등록 기념 이벤트가 진행중입니다. 많은 참여 부탁드립니다~

Post to Twitter Post to Delicious

, , , , , , , ,

iPhone, UITableViewCell에 텍스트 설정하기

3월 16th, 2010

UITableViewCell의 setText 메소드는 아이폰 OS 3.0부터는 비권장(deprecated)하고 있습니다. 따라서 아래의 코드는 3.0 이상인 경우 컴파일은 되지만 경고(warning)이 발생합니다.

1
2
3
UITableViewCell* cell;
// ...
[ cell setText: @"Hello" ];

3.0부터는 textLabel 메소드로부터 반환 받은 UILabel 객체에 텍스트를 설정하도록 변경되었습니다.

1
2
3
4
UITableViewCell* cell;
// ...
UILabel* label = [ cell textLabel ];
[ label setText: @"Hello" ];

이 경우 버전 2.1에서는 컴파일 오류가 발생합니다. 하지만 국내에 출시된 아이폰의 경우 3.1부터 시작하기 때문에 하위 호환 문제는 크게 신경쓰지 않아도 될 듯 합니다.

Post to Twitter Post to Delicious

,

iPhone, XML 처리 예제 – NSXMLParser 이용하기

3월 10th, 2010

몇일 전 iPhone에서 HTTP로 XML 데이터 요청하는 예제를 올렸다가 iPhone SDK에서 NSXMLDocument 클래스를 지원하지 않는 다는 것을 확인하고 NSXMLParser 클래스를 이용해서 XML 데이터를 처리하는 예제를 다시 준비했습니다. 아마도 iPhone에서 NSXMLDocument를 지원하지 않는 건 NSXMLDocument가 메모리를 많이 먹기 때문인 아닐까 싶습니다.

NSXMLParser는 XML 문서를 순차적으로 분석(parse)하면서 이벤트별 처리를 위임(delegate)하는 방식입니다. 따라서 NSXMLDocument를 사용하는 예제보다는 훨씬 더 복잡합니다. 참고로 iPhone에서 사용할 수 있는 XML 관련 라이브러리들이 개발되어 있기 때문에 해당 라이브러리를 익혀서 사용하는 것이 개발 효율은 더 좋을 것 같습니다. 하지만 NSXMLParser를 이용해서 구현하면 메모리도 적게 먹고 나름 성능 향상을 기대할 수 기대할 수 있기 때문에 직접 작성하는 것도 알아두면 좋을 듯 합니다.

예제에 포함된 파일 목록은 아래와 같습니다.

파일 설명
showrooms.xml XML 파일
Dealer.h
Dealer.m
Dealer 모델 클래스
Showroom.h
Showroom.m
Showroom 모델 클래스
ShowroomsXmlParse.h
ShowroomsXmlParse.m
showrooms.xml용 파서 클래스
showrooms_xml_parser.m 실행(main) 소스

우선 showrooms.xml은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8" ?>
 
<showrooms>
  <showroom>
    <name>강남전시장</name>
    <phone>02-111-1111</phone>
    <dealer>
      <name>김태연</name>
    </dealer>
  </showroom>
  <showroom>
    <name>삼성전시장</name>
    <phone>02-222-2222</phone>
    <dealer>
      <name>구하라</name>
    </dealer>
  </showroom>
</showrooms>

예제에서 showroom 및 dealer 엘리먼트가 각각 name 엘리먼트를 자식으로 가지도록 작성했습니다.

Dealer.h와 Dealer.m은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
#import <Foundation/Foundation.h>
 
 
@interface Dealer : NSObject
{
  NSString* _name;
}
  @property( copy ) NSString* name;
@end
1
2
3
4
5
6
#import "Dealer.h"
 
 
@implementation Dealer
  @synthesize name = _name;
@end

Showroom.h와 Showroom.m은 다음과 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#import "Dealer.h"
 
 
@interface Showroom : NSObject
{
  NSString* _name;
  NSString* _phone;
  Dealer* _dealer;
}
  @property( copy ) NSString* name;
  @property( copy ) NSString* phone;
  @property( retain ) Dealer* dealer;
@end
1
2
3
4
5
6
7
8
#import "Showroom.h"
 
 
@implementation Showroom
  @synthesize name = _name;
  @synthesize phone = _phone;
  @synthesize dealer = _dealer;
@end

NSXMLParser는 NSXMLDocument의 NSXMLNode처럼 모델이 없기 때문에 분석(parse) 결과를 담을 수 있는 모델이 필요합니다. 이 모델 클래스들은 MVC에도 적용될 수 있기 때문에 모델 클래스를 만들어 두면 조금 더 우아한 프로그래밍할 수 있습니다. 모델 클래스들은 간단하기 때문에 자세한 설명은 생략하도록 하겠습니다.

ShowroomsXmlParser.h는 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import "Showroom.h"
 
 
@interface ShowroomsXmlParser : NSObject
{
  NSMutableArray* _showrooms;
  NSMutableArray* _elementStack;
}
  - (void) parser: (NSXMLParser*) parser 
      didStartElement: (NSString*) elementName 
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qualifiedName 
      attributes: (NSDictionary*) attributeDict;
  - (void) parser: (NSXMLParser*) parser
      foundCharacters: (NSString*) string;
  - (void) parser: (NSXMLParser*) parser 
      didEndElement: (NSString*) elementName
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qName;
  - (NSMutableArray*) parseContentOfUrl: (NSURL*) url;
@end

ShowroomsXmlParser는 NSXMLParser가 발생시키는 event를 처리해서 Showroom 객체 배열을 넘겨주는 클래스입니다. NSXMLParser가 위임(delegate)하는 method들을 처리하기 위해서 NSXMLParserDelegate 프로토콜을 구현해야 합니다. 참고로 프로토콜은 자바에서 interface랑 동일합니다. 멤버 변수 _elementStack은 파서의 트리 탐색 경로 및 정보를 담아두는 스택입니다.

ShowroomsXmlParse.m은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
#import "ShowroomsXmlParser.h"
 
 
NSString* const SHOWROOM = @"showroom";
NSString* const DEALER = @"dealer";
NSString* const NAME = @"name";
NSString* const PHONE = @"phone";
 
 
@implementation ShowroomsXmlParser
  - (void) parser: (NSXMLParser*) parser 
      didStartElement: (NSString*) elementName 
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qualifiedName 
      attributes: (NSDictionary*) attributeDict
  {
    if ( TRUE == [ elementName isEqualToString: SHOWROOM ] )
    {
      Showroom* showroom = [ Showroom new ];
      [ _elementStack addObject: showroom ];
    }
    else if ( TRUE == [ elementName isEqualToString: DEALER ] )
    {
      Dealer* dealer = [ Dealer new ];
      Showroom* showroom = (Showroom*)[ _elementStack lastObject ];
      [ showroom setDealer: dealer ];
 
      [ _elementStack addObject: dealer ];
    }
    else if ( TRUE == [ elementName isEqualToString: NAME ] ||
      TRUE == [ elementName isEqualToString: PHONE ] )
    {
      NSString* element = [ NSString stringWithString: elementName ];
      [ _elementStack addObject: element ];
    }
  }
 
  - (void) parser: (NSXMLParser*) parser
      foundCharacters: (NSString*) string
  {
    NSCharacterSet* characterSet = [ NSCharacterSet 
      whitespaceAndNewlineCharacterSet ];
    NSString* trimmedValue = [ string stringByTrimmingCharactersInSet:
      characterSet ];
    if ( 0 == [ trimmedValue length ] )
    {
      return;
    }
 
    id element = [ _elementStack lastObject ];
    if ( TRUE == [ element isKindOfClass: [ NSString class ] ] )
    {
      NSUInteger count = [ _elementStack count ];
      NSUInteger index = count - 2;
      id parentElement = [ _elementStack objectAtIndex: index ];
 
      if ( TRUE == [ element isEqualToString: PHONE ] )
      {
        Showroom* showroom = (Showroom*)parentElement;
        [ showroom setPhone: trimmedValue ];
      }
      else if ( TRUE == [ element isEqualToString: NAME ] )
      {
        if ( TRUE == [ parentElement isMemberOfClass: [ Showroom class ] ] )
        {
          Showroom* showroom = (Showroom*)parentElement;
          [ showroom setName: trimmedValue ];
        }
        else if ( TRUE == [ parentElement isMemberOfClass: [ Dealer class ] ] )
        {
          Dealer* dealer = (Dealer*)parentElement;
          [ dealer setName: trimmedValue ];
        }
      }
    }
  }
 
  - (void) parser: (NSXMLParser*) parser 
      didEndElement: (NSString*) elementName
      namespaceURI: (NSString*) namespaceURI 
      qualifiedName: (NSString*) qName
  {
    if ( TRUE == [ elementName isEqualToString: SHOWROOM ] )
    {
      Showroom* showroom = (Showroom*)[ _elementStack lastObject ];
      [ _showrooms addObject: showroom ];
      [ _elementStack removeLastObject ];
    }
    else if ( TRUE == [ elementName isEqualToString: DEALER ] ||
      TRUE == [ elementName isEqualToString: NAME ] ||
      TRUE == [ elementName isEqualToString: PHONE ] )
    {
      [ _elementStack removeLastObject ];
    }
  }
 
  - (NSMutableArray*) parseContentOfUrl: (NSURL*) url
  {
    NSXMLParser* xmlParser = [ [ NSXMLParser alloc ] 
      initWithContentsOfURL: url ];
    [ xmlParser setDelegate: self ];
 
    _showrooms = [ NSMutableArray arrayWithCapacity: 1024 ];
    _elementStack = [ NSMutableArray arrayWithCapacity: 1024 ];
 
    [ xmlParser parse ];
 
    return _showrooms;
  }
@end

NSXMLParser가 동작하기 위해서는 최소한 아래의 메소드들이 구현되어야 합니다.

didStart는 파서가 엘리먼트의 시작 태그를 만났을 때 호출됩니다. 비단말(nonterminal) 노드인 경우 엘리먼트 해당하는 모델 객체를 생성한 후 스택에 넣습니다. 단말(terminal) 노드인 경우 엘리먼트 이름으로 NSString 객체를 만들 후 스택에 넣습니다.

found는 파서가 엘리먼트를 제외한 스트링을 만날을 때 호출됩니다. 인자로 넘어온 스트링의 앞뒤 공백을 제거합니다. 유효한 스트링(이하 속성값)인 경우 스택에서 마지막으로 추가된 NSString 객체를 제거합니다. 제거된 NSString 객체의 값에 따라 적절한 부모 모델 객체의 속성(property)을 속성값으로 설정합니다. 만약 엘리먼트 이름이 NAME인 경우 중복되기 때문에 부모 모델 객체 클래스에 확인합니다.

didEnd는 파서가 엘리먼트의 종료 태그를 만났을 때 호출됩니다. 스택에서 모델 객체를 제거하고 SHOWROOM인 경우 _showrooms 배열 객체에 해당 객체를 추가합니다.

끝으로 콘솔 화면에서 결과를 확인할 수 있는 showrooms_xml_parser.m 파일은 아래와 같습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#import "ShowroomsXmlParser.h"
 
 
int main( int argc, char* argv[] )
{
  NSAutoreleasePool* pool = [ NSAutoreleasePool new ];
 
  ShowroomsXmlParser* xmlParser = [ ShowroomsXmlParser new ];
  NSURL* url = [ NSURL URLWithString: 
    @"http://www.yourserver.com/showrooms.xml" ];
  NSMutableArray* showrooms = [ xmlParser parseContentOfUrl: url ];
 
  for ( Showroom* showroom in showrooms )
  {
    Dealer* dealer = [ showroom dealer ];
    NSLog( @"%@ %@ (%@)", [ showroom name ], [ showroom phone ], 
      [ dealer name ] );
  }
 
  [ xmlParser release ];
  [ url release ];
 
  [ pool drain ];
  return 0;
}

해당 소스들을 컴파일 하기 위해서는 아래의 명령어를 실행합니다.

1
2
$ gcc -framework Foundation Dealer.m Showroom.m ShowroomsXmlParser.m showrooms_xml_parser.m
-o showrooms_xml_parser

실행 결과 화면은 아래와 같습니다.

1
2
3
$ ./showrooms_xml_parser
강남전시장 02-111-1111 (김태연)
삼성전시장 02-222-2222 (구하라)

프로그래밍에 도움되길 바랍니다~

수정 사항

엘리먼트 값이 없는 경우 에러가 발생하는 문제를 해결하기 위해서 showrooms_xml_parser.m의 found 메소드 안에 _elementStack의 마지막 요소 제거 코드를 didEnd로 옮겼습니다.

Post to Twitter Post to Delicious

, , , ,