20111105 :: Learning C – 변수의 종류

아 이거 다 늙어서(?) C언어 공부하려니 훽훽 안돌아가는 내 머리가 원망스러울 따름이고 ㅠㅠ. 어쨌든 이 글은 변수를 설명하는 강좌라기 보다는 C 소스를 볼 때 마법처럼(?) 느껴지는 여러 용어에 대한 이해를 돕기 위한 메모차원의 포스팅

변수

많은 프로그래밍 서적들이 설명하듯이 변수는 어떤 값을 보관하는 상자나 그릇 같은 것이다. 물론 이런 비유는 상당히 시각화하기 쉽기 때문에 이 추상적인 ‘변수’라는 개념을 좀 더 와닿게 느끼게 하는데는 도움을 준다. 다만 조금 더 정확하게 이야기하자면 변수는 특정한 값을 저장하기 위해 마련해 놓은 메모리상의 영역이다. 왜 쉬운말을 다시 어렵게 풀어보느냐면 C를 비롯하여 다양한 프로그래밍 언어들이 변수를 다루는 방법은 앞서 이야기한 ‘그릇 모델’로 이해하기에는 한계가 있기 때문이다. 특히 변수의 유형마다 사용하는 메모리의 단위가 다른 C와 같은 언어에서는 저러한 비유는 초심자에게 팍 와닿을지는 몰라도 조금 위험한 비유인 것이 사실이다. 사실상 저 그릇모델은 C의 복병인 포인터를 이해하는 데 발목을 잡는 역할을 톡톡히 한다.

C언어는 애초부터 편의성보다는 성능에 중점을 두고 설계된 언어이기 때문에 변수를 이해하는데는 메모리에 대한 약간의 지식이 (사실상) 절대적으로 필요하다. 또한 역시 이 ‘성능’과 ‘효율성’에 중점을 두고 있는 연유로 메모리 낭비를 줄이기 위해서 다양한 변수 유형이 존재하는 데, 이러한 변수 유형은 하나의 변수가 차지하는 메모리의 크기를 따로 따로 가진다. 큰 데이터는 큰 그릇에, 작은 데이터는 종지에 담아야 메모리를 절약할 수 있는게 당연한 것 아니겠는가.

C의 변수들은 크게 기본형과 유도형으로 나누어진다는 사실에서 출발하자. 기본형이란 하나의 단일 값을 기억하는 변수이며, 유도형은 이러한 기본형으로부터 확장되는 변수 타입이다.

먼저 기본형에는 정수형, 문자형, 실수형, 열거형, void형 등이 있다. 가장 먼저 살펴볼 것은 정수형이다. 정수를 뜻하는 영어단어 integer의 머리글자를 따서 int 형이라고 하고 보통 4바이트의 크기를 갖는다. 물론 아주 작은 정수를 담는 short int 와 같은 형도 있는데, 일단 요즘 컴퓨터들은 메모리를 좀 넉넉히 가지고 있다고 가정하고 뭐 그건 굳이 신경쓰지 말자.

정수형 변수

정수는 우리가 중학교에서 배웠듯이 자연수와 0, 그리고 자연수의 반대편에 있는 음수들을 이야기한다. 이들은 소수점 이하의 값을 가지지 않는 그냥 숫자와 부호만으로 이루어진 값들이다. 물론 수학시간에 배우는 정수는 무한하게 많은 범위를 가지고 있지만 실질적으로 int 형의 변수가 담을 수 있는 정수의 크기에는 한계가 있다.이건 하나의 변수에 들어가는 숫자의 개수가 유한할 수 없다기 보다는 역시 ‘메모리’를 사용하는 방식 때문이다.

우리가 컴퓨터에서 용량의 단위를 이야기하는 바이트(Byte)를 기준으로 살펴보자. 어렴풋이 기억이 날지는 몰라도 Byte는 8개의 비트가 이루어져서 모이는 단위다. (1 Byte = 8비트) 여기서 비트는 컴퓨터가 좋아하는 0과 1. 이 둘 중 하나의 값을 가리키는 단위이다. 한 개 비트는 2진수의 단위이다.

하나의 바이트는 예를 들면 “00000001“과 같이 8개의 비트(0 혹은 1)이 모여서 구성되는데, 그렇다면 00000000 에서 11111111 까지에 이르는 모든 0과 1의 조합을 구분할 수 있는 단위가 된다. 이는 쉽게 계산하면 28이 되어 256까지의 값을 표현할 수 있다. 즉 만약 어떤 변수가 1바이트의 크기를 가진다고 하면 이 안에는 0~255까지의 256가지의 정보에 한정하여 값을 넣을 수 있다는 의미이다. 솔직히 인간적으로 이 범위는 너무 작으므로 C에서는 정수형 변수에 4바이트의 메모리를 할당한다. 즉 232까지의 범위, 그러니까 0에서 40억 정도까지의 값을 포함할 수 있다. 이 정도면 쓸만한 것 같다. 아 그런데 뭐 빼 먹은 것 없는가? 맞다 정수에는 음수도 있다고 했다. 그래서 하나의 예를 들면 하나의 정수형 변수에는 대충 다음과 같은 값이 들어갈 수 있는데,

00000000 00000000 00000000 10101010

여기서 맨 처음의 0을 +/-를 구분하는 값으로 정하는 거다. 그러면 맨 처음 비트가 1이면 음수, 0이면 양수 이런 식으로 구분할 수 있지 않겠는가? 하지만 4바이트라는 한정된 공간에서 구분할 수 있는 값의 가지수는 일정하기 때문에 -20억~20억 사이의 범위를 표현하는 방법도 있다. 당연히 20억보다 큰 수를 쓰는 경우라면 부호를 포기해야 한다. 따라서 변수형은 제각각 unsigned 인지 signed 인지를 구분하여 사용할 수도 있게끔 해 두었다. (기본적으로는 음수를 쓰게 될 가능성이 크므로 부호가 있는 4바이트 정수형이 기본이 된다.)

그리고 변수는 미리 선언해 두어야 쓸 수 있다. 무슨 소리냐면 C 컴파일러는 미리 언질을 해주지 않은 이름에 대해서는 너무 당혹스러워하면서 에러를 뱉어낸다. 따라서 변수든, 함수든 미리 이런게 있다라고 이야기해주어야 하는 것이다. 변수의 선언은 다음과 같이 한다.

변수형 변수이름;

예를 들어 int 타입의 i라는 변수를 선언한다면,

int i;

라고 선언을 해준다. 물론 같은 형의 변수라면

int i, j, k, l;

과 같이 컴마로 구분하여 여러 변수를 한 번에 선언할 수도 있다. 또한

int i=0;

이렇게 변수를 선언하면서 동시에 어떤 값을 집어 넣는 것도 가능하다. 이건 변수를 선언하면서 초기화했다고 한다. 초기화가 뭔지는 묻지 말자 이 아래로 써야 할 글이 수백만 줄인 것 같은 기분이다.

실수형 변수

소수점이 있는 수로 범위를 확장하면 조금 얘기가 달라진다. 소수점이 있는 수는 그 자리수가 유한한지 무한한지에 따라 유리수와 무리수로 나뉘는데 이를 통틀어 실수라 한다. 잠깐 생각해보자. 0과 1사이에는 몇 개의 실수가 있을까? 당연히 무수히 많이 있다. 그런데 변수의 크기는 이런 많은 수들을 구분짓는 종류와 직결된다. 결국 정수처럼 이런 실수를 다루려고 한다면 우리는 아무리 좋은 컴퓨터가 있어도 안된다. 1/3의 계산을 할 수가 없게된다. 그래서 실수는 정수와는 다른 방식을 사용하여 저장하고 표현한다. 학교 때 잠깐 배운적이 있는 2.4534635645 * 109 과 같은 식으로 특정한 한계까지의 자리수와 몇 자리로 이루어진 수인지의 정보를 조합하여 이런 실수를 표현한다. 물론 덕분이 소수점 아주 많이 아래로 내려가거나 하는 경우는 구분할 수 없는 지경에 이르므로 어느 정도의 정확도 손실을 감안해야 한다. (이러한 정밀한 계산을 위해서는 일반적인 사칙연산을 하는 방법이 아닌 다른 방법을 통해 계산한다.)

아참, 그리고 이러한 실수 표현 방식을 부동소수점이라 하는데 (소수점이 움직이지 않는게 아니라 둥둥 떠다닌다는 의미다.) 이게 컴퓨터가 다루기에 참 힘든 종류의 계산이라 힘이 좋은 CPU를 만들기 위해서 언제부터인가 이런 계산에 특화된 회로를 달고 나오게 된다. 아주 오래전에 인텔의 펜티엄 프로세서가 이 계산을 제대로 못한다고 세상의 비웃음을 산 적이 있다.

실수형 변수의 종류에는 floatdouble 이 있다. 물론 각각은 signed/unsigned로 다시 구분이 되며, 보다 큰 범위를 위한 long 형과 조합을 하는 경우도 있는데 역시나 나같은 사람에겐 어울리지 않으니까 패스.

문자형 변수

문자형 변수는 긴말하지 않겠다. 여기서 말이 길어지면 왜 우리 선조들이 컴퓨터를 만들지 못했나 하는 원망부터 시작해서 온갖 인코딩과 유니코드까지 갈것만 같다. 이에 대해서는 조엘 아저씨가 멋진 글을 쓴 적이 있다. 아무튼 이 컴퓨터를 만든 미쿡사람들은 사용하는 문자가 52개 밖에 없다. 그외에 우리 생활에 쓰이거나 정말 한 번도 보지 못한 문자들을 다 갖다 모아도 128자 안에는 다 때려박을 수 있었다. (그게 아스키코드다) 그래서 문자형 변수는 고작 1바이트만 쓴다. 한글은 이보다 훨씬 많은 자모조합을 가지고 있다. 그래서 2바이트로 써야 한다. 이건 나중에 문자열에서 다시 기회가 있으면 이야기하자.문자형 변수는 char 라고 한다. 당연히 character의 줄임이겠지.

void 형

void는 “비어있는”이라는 의미이다. 타입이 비어있다는 말은 뭐가 들어올지 모른다는 이야긴데. 어렵다. 이건 넘어가자. 유형이 없으므로 어떤 값이 담겨도 그게 1이라는 수인지 어떤 글자인지, 뭔지 알 수가 없다.

열거형

이 글을 쓰고 앉아있게 된 근본원인은 열거형이다. 열거(enumerous)라는 단어도 생소한데 상당히 요상하게 쓰여서 이게 꼭 마법의 주문 같고 무슨 말인지 모르겠다는게 문제. 예를 들면 이런 건데

enum {EAST, WEST, SOUTH, NORTH} direction;

이건 direction이라는 변수를 열거형으로 선언한 것이다. 열거형은 사실 정수형과 똑같은 것인데 한가지 재밌는 것은 direction=12 이런 식으로 넣는 것은 안되고 저기 중괄호 안에 들어있는 단어만 넣을 수 있다는 점이다. 즉 direction=0 이라고 해 놓으면 나중에 보는 사람이 이게 무슨 의미인지 도통 알 수가 없지만 direction=EAST 라고 하면 그 의미가 딱 눈에 들어오지 않는가? 중괄호 속에 나열된 단어들은 각각 0, 1, 2, 3과 같은 식으로 증가하는 정수 값이 된다. 물론 이 값들을 계산에 사용할 수도 있는데 그 경우를 위해서 각각의 값이 실제로는 얼마인지 정의해줄 수도 있다.

enum {EAST=11, WEST=13, SOUTH=13, NORTH} direction;

NORTH는 13다음이니 14를 의미하게 된다. WEST와 SOUTH는 같은 값으로 썼는데 이건 에러가 나는 게 아니라 두 단어를 동의어로 간주하게 된다. C가 이렇게 친절할 수 있는 것일까. 심지어는 다음과 같은 방식으로도 쓰는데

enum origin {EAST, WEST, SOUTH, NORTH} ;

이때 origin은 태그라고 하는데 이 태그는 마치 변수의 타입처럼 동작한다. 따라서 origin direction; 이라고 변수를 선언하면 저 열거형을 그대로 사용할 수 있게 되는 것이다.

사용자 정의형

여기서 한 번 더 마법의 단어가 등장한다. C는 심지어 특정한 타입의 변수를 사용자가 정의할 수 있게 하는데 그것이 사용자 정의형이다. 예를 들면 나는 int 가 무슨 말인지 모르겠으니 jungsoo 라는 말로 풀어 쓰고 싶다고 하면 다음과 같이 jungsoo 라는 유형을 정하면 된다.

typedef int jungsoo;

사실, 사용자 정의형은 일종의 줄임말 같은 것이다. 여기서는 int 형이 아닌 jungsoo형을 만들었다. 물론 두 개는 같은 거다. 이는 이런 단순한 유형보다는 뒤에 나올 구조체나 그런 복잡한 유도형을 간단히 줄여서 쓸 데 사용한다. 그러니까 이제는 무슨 말인지 알았으니 놀라지 말자.

유도형

앞에서 유도형은 기본형 변수를 확장한 형태라고 했다. 그 유명한 배열 / 구조체 / 공용체 / 포인터  같은 것들이 유도형 변수에 속한다. 기본형에 대해 쓰는데도 저렇게 많이 썼는데.. ㅠㅠ

배열

배열은 C가 가장 사랑하는 변수 유형이다. 그리고 또 매우 흔하며, 처음에는 쉽다가 나중에는 머리 아픈 형이다. 배열은 동일한 타입을 가지는 변수가 연속해서 붙어있는 덩어리이다. 이건 마치…. 속옷 정리함 같은 그런 개념이다!!!

여기서 포인트는 동일한 타입을 붙여 놓았다는 것이다. 즉 정수면 정수, 실수면 실수, 문자면 문자… 이렇게 같은 종류만 모아서 취급한다. 이런 제약이 있지만 구조가 매우 단순하고 또 “메모리에서 연속해 있기 때문에” 아주 빠르게 돌아간다. 배열 속에 뭔가 주루룩 넣어 두고 반복해서 작업을 처리해야 할 때 배열은 매우 잘 어울린다. 배열을 구성하는 각각의 단위는 Element라고 하는데 우리말로는 ‘원소’는 좀 이상하고 ‘요소’도 좀 그렇다.

배열의 선언은 생각보다 단순한데,

타입 배열이름 [크기][크기]….;

각각 요소가 어떤 타입인지를 말하고 이름을 쓴 다음 뒤에 대괄호를 쓰고 그 속에 몇 개짜리 집합인지를 써 주면 된다. 또한 이런 배열을 중첩해서 다시 배열로 만드는 것이 가능해서 뒤에 대괄호를 계속 쓸 수도 있다.

이렇게 크기를 미리 지정해주는 것은 C에서 매우 중요한데, 왜냐면 변수는 메모리의 영역이고 크기를 지정해 주어야 컴파일러가 적당한 메모리 구간에 우리가 자료를 넣을 수 있도록 준비해 줄 수 있는 것이다. 다만, [크기]를 한 번은 생략하고 []만 적는 것도 가능하다. 그럼 컴파일러는 적당한 시점에 그 크기를 정해서 메모리를 할당해 준다. (이런 기능이 있는 것만으로도 감사해야 할 지경이다.)

int arr[5];

컴파일러는 이 배열 선언문을 만나면 정수형 변수 5개의 영역만큼 메모리 안의 연속된 공간을 미리 찜해준다. 각각의 칸은 4바이트니까 이 배열은 메모리에서 20바이트를 차지하는 셈이다.

배열의 각 요소를 참조하는 방법으로는 배열이름에 [순서]를 적어주는 것이다. arr[1] 이라고 하면 위의 배열에서 두 번째 칸을 지칭한다. 컴퓨터는 거의 대부분의 언어에서 0부터 시작해서 순서를 센다. (애플 스크립트는 1부터 세던데, 이는 그 언어가 무척이나 사람의 자연어와 가깝게 만들려고 했기 때문일 거다.)

참고로 C에는 “문자열”이라는 변수형은 없다. 문자열을 다루려면 문자형 배열을 써야 한다. (아 이게 또 골치아프지) 만약에 computer라는 7글자짜리 단어를 저장하고 싶다면 다음과 같이 선언해야 한다.

char com[8];

7글자인데 왜 8칸짜리 배열이 필요한가? 컴퓨터에게 이 배열은 그냥 문자가 각각 다른 변수에 들어있고 그게 붙어있는 속옷 정리함 같은 것이라 했다. 그래서 이것을 문자열로 인식하기 위해서는 문자열이 어디서 끝나는지 알아야 한다. (컴파일러는 배열이 메모리상에서 어디서 시작하는 것에는 관심이 있지만 어디서 끝나는지는 관심이 없다.) 따라서 이 배열의 맨 마지막에는 null 문자가 들어간다. null은 ‘없음’을 의미하는 개념인데 그렇다고 아예 없는 것은 아니고 null 이라는 개념으로 존재한다. 아무튼 이게 맨 끝에 있어야 저 배열이 문자열이구나하는 것을 알게 된다는 것만 알아두고 넘어가도록 하자.

구조체

구조체는 배열하고 약간 다르다. 배열은 여러번 강조했지만 같은 타입의 변수를 모아놓은 집합체이다. 구조체는 서로 다른 타입의 집합체이다. 구조체를 만드는 키워드는 struct 인데, 대략 다음과 같이 선언한다.

struct {

    char Name[10];
int Age;
float Height;

} friend;

friend 라는 변수는 구조체 변수인데, 그 속에는 이름, 나이, 키를 넣을 수 있는 변수가 멤버로 속해있다. (구조체 안에 들어간 변수를 멤버라 한다.) 구조체는 기본형외에도 배열이나 다른 구조체를 그 멤버로 가질 수 있다. 멤버는 선언된 순서대로 메모리에 할당된다. 만약 friend의 나이를 처리하고 싶다면 friend.Age와 같은 식으로 구두점(.)을 사용하여 두 이름을 연결하면 된다. (이건 다른 언어들에서도 많이 봤으니 패스)

구조체를 매번 저런 형태로 선언하는 게 좀 불편하니까 보통은 구조체 자체를 배열로 선언하여 사용하거나, 혹은 사용자 정의형을 사용하여 구조체 자체를 특정한 형식으로 만들어서 사용하는 것을 많이 쓴다.

typedef struct {
char Name[10];
int Age;
float Height;
} friend;  //이 friend가 변수명이 아닌 타입명이 된다.

friend kimchulsoo, hongkildong;

이런 식으로 특정한 구조체를 타입으로 만들어서 사용하는 방법이 그나마 많이 보이는 패턴인 듯 하다.

공용체는 구조체와 유사한데, 멤버끼리 기억공간을 공유한다. 그런데 이게 너무 어렵다. 사실 아직까지는 공용체를 사용하는 소스를 본 적이 없기도 하고 여러 자료를 찾아봐도 감이 안오니 패스.

포인터

드디어 포인터다. C에서 가장 어려운 개념중에 하나이자, 가장 중요한 개념이라고 한다. C언어를 누구는 고급언어라고 하는데[1. 고급언어라는 말은 ‘고수준언어’로 고쳐써야 할 것이다. 꼭 C가 완전 대단한 사람들만 쓰는 언어라고 생각이 들게 하는가.]

아까 정수형 변수 설명으로 돌아가본다. 변수를 선언하면 컴파일러는 해당 변수 유형의 크기만큼의 공간을 메모리에 할당한다고 했다. 그러면 나중에 그 변수의 값을 다시 찾아와야 하거나, 혹은 그 자리에 다른 값을 바꿔 집어 넣어야 한다면 어떻게 그 영역을 찾을 수 있을까? 그게 가능한 것은 모든 메모리의 바이트에는 주소가 있기 때문이다. 메모리상에서의 주소, 즉 위치를 표시하는 값을 번지(address)라고 한다. 포인터는 이 메모리의 번지를 기억하는 변수이다.

물론 이렇게 말로는 정의할 수 있지만, 이건 “그래서 뭐 어쩌라고” 같은 그런 느낌이다. 그래서 좀 적절하지는 않지만 비유를 들어서 생각해보자면 도서관에 책들을 생각해보면 된다.

도서관에는 각 분류별로 책을 나누고 다시 이렇게 나누어진 책들을 제목순으로 정리한다. 그렇게하면서 책을 쉽게 찾게하기 위해서 서지번호라는 걸 매긴다. 우리는 도서관에서 책을 찾을 때 이 서지번호를 이용해서 책을 찾게 된다. 하지만 단순히 제목만으로는 도서관에서 책을 찾을 수가 없다. 왜냐면 도서관 전체를 봤을 땐 책들이 어떤 기준으로 분류되어 있는지 명확하지 않기 때문이다. 따라서 서지번호를 통해 간접적으로 책을 찾는 것이다. 이는 ‘정렬’과 같은 경우에 상당히 유용해질 수 있는 개념인데, 도서관의 모든 책을 제목순으로 정렬하는 것은 만만한 일이 아니다. 설령 그렇게 큰 작업을 해 놓더라도 다시 저자순으로 정렬하거나 출판년도 순으로 정렬을 다시하려면 엄두가 안날 것이다. 하지만 서지 번호가 적힌 쪽지를 그렇게 정렬하는 것은 비교적 쉬운 일이다. 즉 포인터는 실제 책을 가리키는 서지번호에 해당된다고 생각하면 된다.

한가지 재미있는 사실은 배열에서 배열명 그 자체는 배열이 시작하는 메모리 번지를 가리키는 포인터가 된다. 그래서 이런 우스꽝스러운 코드는 C 문법상 하자가 없으며, 심지어 제대로 동작하기도 한다.

int a[10] = {0,1,2,3,4,5,6,7,8,9};
int i;
for(i=0;i<10;i++){
    printf("%d", i[a]);
}

20111220 :: [iOS] 저장이 가능한 간단 메모장 2 (1/2)

지난 글에서 간략한 예제로 만들었던 내용에 이어 오늘은 두 번째 시간. 두 번째 시간을 시작하기에 앞서 다뤘던 내용은 iOS에서 사용자의 데이터를 저장하는 방법과 관련하여 몇 가지를 알아보았고, 그 중에서 비교적 쉽게 접근할 수 있을 것으로 보이는 ‘아카이빙’에 대해 알아보았다.

실제로 아카이빙은 대부분의 코코아터치 객체들이 자신을 아카이빙하는 방법을 알고 있기 때문에 루트 객체를 아카이브하면 자동으로 엮여있는 모든 정보가 아카이브되고, 이것을 간단히 NSKeyedArchiverNSKeyedUnarchiver를 사용하여 직렬화된 정보를 파일에 쓸 수 있다는 것 까지 확인해 보았다.

오늘은 이 메모장을 조금 더 확장하여 여러 개의 메모를 생성하고 이를 한꺼번에 저장할 수 있는 메모장을 만들어볼 것이다. 사실 이 부분에서 가장 주요한 내용은 각각의 메모를 대표하는 클래스를 하나 만들어서 이 클래스가 어떻게 스스로를 아카이빙하는지를 기술해주기만 하면 되는 내용이고 나머지 대부분의 내용은 테이블 뷰를 사용하는 방법이나 앱 델리게이트를 사용하는 방법에 대한 내용이 될 것 같다. 내용들이 많고 글이 길어질 것 같으나 적당한 분량에서 조절하도록 하겠다.

프로젝트 시작

기존에 만들어 둔 내용을 토대로해도 무관하나, 그냥 새로 만들어보도록 한다. 싱글뷰 기반(윈도 기반) 프로젝트로 시작해도 되고, 빈 프로젝트로 시작해도 된다. 빈 프로젝트에서 시작하는 경우에는 스토리보드를 추가하여 UI를 구성하는 것이 “쉽다”.1빈 프로젝트에서 스토리 보드를 추가하여 사용하는 방법은 이전에 올린 포스팅을 참고로 한다.

메모 클래스

각각의 메모는 단지 하나의 텍스트가 아니라 제목, 본문, 최종수정일의 정보를 갖는 정보의 덩어리로 생각할 수 있다. 따라서 이는 별도의 클래스로 만든다.

프로토콜

커스텀 클래스를 정의해서, 이 객체들을 아카이빙하기 위해서는 이 객체들이 NSCoding 프로토콜을 따르도록 해야 한다. 프로토콜을 설명하기에는 이 글은 다른 내용이 너무 기니까 그건 따로 언급하지 않겠다. 어쨌든 NSCoding 프로토콜은 2개의 필수 메소드를 선언하고 있는데 그것은 -encodeWithcoder: 와  -initWithCoder: 의 두 개 메소드이다. 각각은 자신을 인코딩하는 방법과 그리고 인코드된 데이터로부터 복원하는 방법을 서술한다. 인코딩은 파일에 저장되는 바이너리 데이터로 객체스스로를, 즉 객체 내부에 존재하는 인스턴스 변수들을 코딩하는 것이고, 디코딩은 바이너리 데이터 스트림으로부터 자료를 뽑아내어 이를 다시 인스턴스 변수에 넣어주는 작업이고 실제 인코딩/디코딩이 이루어지는 과정에 대해서는 직접 관여할 필요가 없다.

다만 실질적으로 어떤 변수를 인코딩하였을 때 추후에 그 값을 그대로 복원하기 위해서는 이름을 주어야 하고 이는 키-밸류 형태의 짝으로 이루어지게 되고 복원할 때에도 그 키를 사용하여 올바른 변수의 값을 되찾게 된다.

메모 클래스 생성

cmd + n 을 눌러 새로운 파일을 하나 만든다. NSObject의 서브 클래스를 만들고 이름은 Memo라고 하자. 알겠지만 Memo.h 와 Memo.m 이 생긴다. 여기에 인스턴스 변수를 정의한다.  각각의 메모는 제목과 메모본문, 두 개의 문자열 인스턴스 변수를 가지며, 인코딩될 때 이 두 문자열 객체를 인코드하게 된다.

///Memo.h

#import <Foundation/Foundation.h>

@interface Memo : NSObject <NSCoding>
@property (retain, nonatomic) NSString *title;
@property (retain, nonatomic) NSString *memotext;
@end

이제 초기화하는 메서드를 붙이고 인스턴스 정보를 인코드/디코드하는 메서드를 추가한다.

///Memo.m

#import "Memo.h"

@implementation Memo
@systhesize title, memotext;

-(id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super init];
    self.memo = [aDecoder decodeObjectForKey:@"memo"];
    self.title = [aDecoder decodeObjectForKey:@"title"];
    return self;
}

-(void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:title forKey:@"title"];
    [aCoder encodeObject:memo forKey:@"memo"];
}

@end

이제 우리의 메모 클래스는 스스로를 인코딩할 수도 있고, 인코딩했던 정보를 다시 불러와서 스스로를 복원할 수도 있게되었다.  위 코드를 잘 살펴보면 NSCoder 클래스가 제공되면 이를 사용하여 인스턴스 변수를 키를 이름으로 주고 인코딩하고, 디코딩시에는 같은 키로 불러온 값을 각자의 변수에 넣어주는 일을 하고 있다.

이외에 인스턴스 변수가 객체가 아닌 정수 및 실수나 다른 객체들을 인코딩하게 되는데 이 때 사용되는 메소드는 encode<값 유형>:forKey: 메소드가 된다. 자세한 내용은 개발자 문서를 참고하자.

또한, Memo 객체는 신규로 생성되는 경우가 있을 수 있으므로 init 메소드를 따로 재정의한다.

-(id) init
{
    self = [super self];
    if(self){
        [self setTitle:@“”];
        [self setMemotext:@“”];
    }
    return self;
}

앱 델리게이트

앱 델리게이트에서는 크게 다음과 같은 기능들을 수행해야 한다.

  • 1) 메모 목록을 테이블 뷰를 통해 구현할 것이다. 이 메모목록을 표시하는데 필요한 메모를 배열로 가지고 있어야 한다.
  • 2) 기존 메모 선택 시 해당 메모를 선택할 수 있는 인덱스를 담는 변수를 가지고 있어야 한다.
  • 3) 앱 초기 런칭시 저장된 파일의 내용을 불러올 수 있어야 한다.
  • 4) 메모의 작성 / 편집 시 저장한다. (삭제때에도 저장한다)

인터페이스

인터페이스 파일에는 다음과 같은 내용을 선언한다. 먼저 파일이 저장되는 경로를 하나 선언한다. 또한 다른 클래스에서 액세스할 수 있도록 메모의 리스트를 담든 배열과 메모의 인덱스를 지정하는 정수를 하나 씩 프로퍼티로 만든다.

@interface AppDelegate : NSObject <UIApplicationDelegate>
{
    NSString *filePath;
}

@property (strong, nonatomic) NSMutableArray *memoListArray;
@property (readwrite, assign) int memoIndex;

-(void)saveData;

@end

구현부

먼저 앱이 실행되면 데이터 파일의 경로를 생성하고, 이에 따라 해당 파일이 있으면 이를 읽어 오는 등의 작업을 처리한다.

-(BOOL)application:didFinishLaunchingWithOptios:
{
    memoListArray = [[NSMutableArray alloc] init];

    NSString *docDir;
    NSArray *dirPaths;
    dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    docsDir = [dirPaths objectAtIndex:0];
    filePath = [[NSString alloc] initWithString:[docsDir stringByAppendingPathComponent:@"data.archive"]];

    NSFileManager *fileManager = [NSFileManager defaultManager];
    if([fileManager fileExistsAtPath:filePath])
    {
        NSArray *savedMemo = [[NSArray alloc] init];
        savedMemo = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
        memoListArray = [savedMemo mutableCopy];
    }

    //    또한, 기본적으로 아무런 메모도 선택되지 않은 상황이므로 인덱스는 -1로 둔다.
    memoIndex = -1;

    return YES;
}

이번에는 데이터를 저장한다. 기본적으로 생성된 메모와 기존 메모는 모두, memoListArray에 들어있다고 가정한 상태이다. -saveData:는 이 배열 전체를 그대로 파일에 저장한다.

앞서 말한바와 같이 배열을 인코딩하면 배열은 모든 요소 객체에 인코딩하라는 메시지를 보낸다. 각 객체는 다시 저장될 인스턴스 변수를 인코딩한다.

-(void)saveData
{
    NSArray *arrayToSave = [NSArray arrayWithArray:(NSArray *)memoListArray];
    if(![NSKeyedArchiver archiveRootObject:arrayToSave toFile:filePath])
    {
        NSLog(@"Fail to save");
    }else{
        NSLog(@"data saved");
    }

    arrayToSave = nil;
}

메모리스트

메모리스트는 테이블뷰를 사용하여 생성한다. (물론 그냥 UIViewController를 사용해서 만들 수도 있다.)
테이블뷰는 다시 네비게이션 컨트롤러로 감싸진다. 테이블 뷰 컨트롤러는 앱 델리게이트에서 메모 리스트를 가져와서 각 메모의 제목을 표시한다. (이는 나중에 구현한다.)

상단 아이템바에는 신규 메모 생성버튼을 만들고 이를 통해 새 메모를 추가한다. 또한 이미 작성된 메모를 탭하면 상세 뷰로 이동하면서 해당 내용을 열람하고, 원한다면 수정을 할 수 있도록 한다.

이 때 상세뷰에서는 별도의 저장버튼은 만들지 않고, 네비게이션 컨트롤러에서 뒤로 돌아갈 때 바로 해당 내용이 저장되도록 한다.

스토리보드

새로 스토리보드를 만들었다면, 비어 있는 스토리보드에 테이블뷰 컨트롤러를 하나 추가한다. 추가된 부 컨트롤러를 선택해서 매뉴에서 Editor > Embed >
Navigation Controller를 선택한다. 신규 생성한 테이블 뷰 앞에 뷰컨트롤러가 나타나고, 테이블뷰 위쪽에도 네비게이션 바가 추가된다. 다음 테이블 뷰 컨트롤러의 네비게이션 바에 바 아이템을 하나 추가한 후 이 버튼의 identify 속성을 add로 준다 (더하기 모양으로 바뀜)

다음 새 뷰 컨트롤러를 하나 더 추가한다. 그런다음 아울렛을 바인딩하는 것과 같은 방법으로 테이블 뷰 컨트롤러의 바 버튼과 새 뷰 컨트롤러를 연결한다. 타입은 Push를 선택한다.

여기까지하고 앱을 빌드하면 초기 화면에 테이블뷰가 보이고 버튼을 선택하면 새 뷰로 이동, 뒤로 버튼을 탭하면 다시 테이블뷰로 돌아오는 것 까지 동작하는 것을 확인할 수 있다.

컨트롤러 작성

이제 컨트롤러를 작성할 차례이다. 새 파일을 만들고 유형을 테이블 뷰 컨트롤러로 선택한다. 파일이름은 RootViewController 정도가 적당하겠다. 이 컨트롤러는 다음과 같은 기능을 수행한다.

  1. 뷰가 나타날 때 메모 리스트 정보를 가져와서 각 셀에 뿌려준다.
  2. 셀을 터치하면 앱델리게이트의 인덱스를 변경, 선택된 메모가 상세 뷰에서 표시되도록 한다.
  3. 셀을 삭제하면 해당 내용이 삭제되고 파일이 업데이트 되도록 한다.

하지만 실질적으로 우선은 새 메모를 추가하는 것만 생각하자. 대신 스토리보드에서 테이블뷰 컨트롤러를 선택해 클래스를 RootViewController로 지정한다.

디테일 뷰 컨트롤러

상세를 보여주고 새 메모를 작성하는 화면으로, 스토리보드에 마지막에 추가한 뷰 컨트롤러이다. 새 파일을 만들고 타입은 뷰컨트롤러가 되게하자. 이름은 DetailViewController가 좋겠다. 다시 스토리보드에서 디테일뷰를 마저 완성하자.

스토리보드

디테일뷰가 될 컨트롤러를 선택해 클래스 이름을 바꿔주고, 텍스트 필드 하나와 텍스트 뷰 하나를 달아준다. 택스트뷰는 지난 번 프로젝트와 마찬가지로 편집이 가능한 editable 속성을 가진다.

UI아울렛을 코드로 작성하기보다는 assistant editor를 열고 해당 인터페이스 정의 부분에 각 텍스트 필드와 뷰를 연결하면 아울렛을 생성하는 대화상자가 나타나니 이를 이용해서 두 개의 아울렛을 추가한다.2

인터페이스

디테일뷰는 현재 선택된 메모의 인스턴스를 가지고 UI에 내용을 그려주며, 편집된 내용을 반영하고 파일에 저장한다. 실제 데이터는 앱 델리게이트가 가지고 있으므로 앱 델리게이트의 인스턴스만 가지면 된다.

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

@class Memo;

@interface DetailViewController : UIViewController
{
    AppDelegate *appDelegate;
    Memo *currentMemo;
}

// 아래는 스토리보드에서 끌어서 아울렛을 만들면 생기는 코드
@property (weak, nonatomic) IBOutlet UITextFiled *title;
@property (weak, nonatomic) IBOutlet UITextView *memotext;

구현부

구현부에서는 뷰컨트롤러가 초기화될 때 해야하는 일을 지정한다. 지금은 “추가” 버튼을 탭하여 디테일 뷰를 연 상황에 대해 코드를 작성한다.

#import “Memo.h” 를 추가해야 한다. 프로퍼티 2개에 대한 @synthesize 구문은 자동으로 작성되어 있을 것이다.

-(id)viewDidLoad
{
    appDelegate = [[UIApplicatioin sharedApplication] delegate];
    // appDelegate의 인스턴스를 얻었다.

    if([appDelegate.memoIndex < −1){
        //    메모의 인덱스가 −1이 아님 (새 메모가 아님)
        currentMemo = [[appDelegate memoListArray] objectAtIndex:[appDelegate memoIndex]];
    } else {
        currentMemo = [[Memo alloc] init];
    }

    [self.title setText:[currentMemo title]];
    [self.memotext setText:[currentMemo memo text]];
}

네비게이션 바의 ‘back’ 버튼을 누르면 작성된 값을 메모 리스트에 추가하고, 배열을 영구저장소에 저장한다. back 버튼을 누를 때는 moveToParentViewController: 가 호출되지만 이 메소드는 디테일뷰로 진입할 때에도 한 번 호출되므로 viewDidDisappera:에 구현하도록 한다. 신규 메모인 경우에는 appDelegate의 memoIndex가 −1이고, 이 경우에는 배열에 새롭게 추가한다.

-(void)viewWillDisappear:(BOOL)animated
{
    [currentMemo setTitle:[self.title text]];
    [currentMemo setMemotext:[self.memotext text]];

    if([appDelegate memoIndex] == −1){
        [[appDelegate memoListArray] insertObjectAtIndex:0];
    }

    [appDelegate saveData];
    [appDeleagte setMemoIndex:-1];
}

이제 앱을 빌드하고 실행하면 동작은 하지만 새로 생성된 메모를 확인할 수는 없다. 아직 테이블뷰의 데이터소스 부분을 구현하지 않았기 때문이다. 테이블뷰의 데이터소스는 추가적인 설명이 더 필요하므로 다음 글에서 계속 잇도록 하겠다.


  1. 많은 개발자들이 스토리보드를 쓰는게 참 안좋다라고 하는데, 코드상에서 UI를 짜는 일이 어려운 초보에게 이 스토리보드가 무척이나 편리한 것은 사실이다. 그렇지만 생각한대로 나오지 않아 삽질을 해야 하는 경우가 없지는 않다는 것이 단점일 수는 있다. 
  2. 이는 Xcode4의 가장 멋진 기능이다. 실컷 코드를 작성했는데 뭔가 동작이 안된다면 아울렛과 인터페이스 상의 객체를 연결하지 않았기 때문일 확률이 큰데, 이 기능은 상당수의 코드를 줄여주면서 이런 실수를 방지하게 해준다. 

20111207 :: [iOS] 저장이 가능한 간단 메모장

iOS 앱이 데이터를 저장하는 방법

많은 튜토리얼에서 간단한 아이폰 앱을 만드는 방법을 설명하고 있는데, 이런 튜토리얼을 따라서 이것 저것 만들어 보는 것 또한 재미도 있고, 또 여러가지 테크닉을 익힐 수 있지만 정작 활용이 가능한 앱을 만드는 것은 쉽지 않다. 이 글 (과 아마도 이어질 글들)에서는 메모장과 같이 간단히 입력한 텍스트를 저장하는 앱을 만들어 보는 것을 함께 알아보고자 한다. 혼자 삽질과 염탐(?)을 거듭하여 알아낸 내용들을 정리하는 차원이기도 하니 아주 자세하게는 아니지만 소상히 쓰려고 노력할 것이다.

iOS 앱이 어떤 데이터를 영구적으로 보관하는 방법에는 사실 여러가지가 있는데 대략 다음과 같은 방법들이 있다.

  • 코어데이터
  • SQLite
  • 아카이빙(직렬화)
  • 프로퍼티 리스트

코어데이터

코어데이터는 데이터를 저장소에 읽고 쓰고 또 관리하는 전반적인 기능을 제공하는 프레임워크이다. 코어데이터는 저장 방법을 아카이브 또는 데이터베이스(SQLite)로 사용할 수 있고, 변경 사항을 쉽게 추적하여 저장하고 또 저장된 객체를 불러와서 자동으로 관리해주는 다양한 기능들을 제공한다. 이를 잘 사용하면 엄청나게 많은 양의 코드를 손쉽게 줄일 수도 있다. 하지만 이 프레임워크는 결정적으로 초보자가 사용하기에는 조금 어렵다. 코어데이터 프레임워크 자체의 개념 자체가 초보자에게는 낯설기 때문이나, 실질적으로 예제를 만들어보면 허무할만큼이나 간단하게 데이터를 처리해준다.  예를 들어 사용자가 편집한 변경사항을 자동으로 기록한다거나 하는 것들을 지원한다.

코어데이터는 OSX용 앱을 제작할 때 코코아 바인딩과 결합하면 “한 줄의 코드도 작성하지 않고” 데이터를 불러오고 편집하고 저장하는 앱을 만드는 것이 가능할 정도의 모든 기능을 제공해주므로 시간이 될 때 꼭 공부해볼 것을 권장하며, 이 연재의 마지막에는 코어데이터를 사용하여 메모앱을 작성하는 방법을 살펴볼 것이다.

SQLite

SQLite는 아이폰에도 내장되어 있는 경량 데이터베이스 엔진이다. 개인적으로는 초보자에게 차라리 코코아 프레임워크를 권하고 싶다. 코코아터치에 포함되어 있는  SQLite3 프레임워크는 대부분 C 형식의 함수를 사용하고 있고 데이터 입출력시에는 일일이 쿼리 문을 작성해야 하므로 SQL에 대한 지식도 알고 있어야 하는 데 난점이 있다. 단, 사전이나 레퍼런스와 같은 형태의 앱을 작성할 때 미리 자료를 정제하여 DB 형태로 만들기가 쉬우므로 이 경우에는 SQLite를 쓰는 것이 좋을 수도 있다.

아카이빙

아카이빙 역시 초보자들에게는 조금 어려운 개념일 수 있으나, 대부분의 표준 코코아터치 클래스들은 스스로를 인코딩하는 방법을 알고 있기 때문에 의외로 손쉽게 접근할 수 있는 방법이나, SQLite에 밀려서 많이 쓰이고 있지 않다. 이 연재에서 우선적으로 다뤄볼 기법이기도 하다.

프로퍼티 리스트

프로퍼티 리스트는 일종의 XML의 포맷으로 키-값 의 짝을 기록하는 방법이다. 이 방법으로 생성된 데이터는 별도의 프로그램 없이 사람이 확인할 수 있다는 장점이 있다. 하지만 나는 잘 모르므로 패스하겠다.

아카이빙

아카이빙은 앱이 실행될 때 서로 서로 관계를 맺고 있는 객체들을 직렬화하여 데이터 스트림으로 만드는 기법이다. 이 데이터 스트림을 파일에 저장하고 또 읽어 들이는 방법으로 사용자 데이터를 보존할 수 있다.

다행히도 많은 표준 코코아터치 클래스들은 스스로를 아카이빙하는 방법을 알고 있다. 이러한 객체들은 NSCoding 프로토콜을 따르는데, 이 프로토콜을 따르는 객체는 스스로를 아카이빙하면서 자신과 연관을 맺고 있는 모든 객체에 대해 아카이빙하라는 메시지를 보낸다. 따라서 우리는 각각의 객체가 NSCoding 프로토콜을 따르도록하고 루트 객체를 아카이빙하면 필요한 모든 객체를 아카이빙할 수 있게 된다. (이와 관련해서는 나중에 다시 살펴볼 계획이다.) 이 방식의 강점은 여러 객체를 저장하기 위해서는 이를 단순히 배열에 담고 그 배열 객체를 한 번만 아카이빙하면 마치 전염병처럼 메시지가 번져나가 모든 객체를 아카이빙 할 수 있다는 것이다. 또한 단순한 값이 아닌 바이너리 데이터도 파일에 함께 그대로 저장할 수 있다는 강점이 있다.

객체를 아카이빙하기 위해서는 NSCoder 클래스를 사용한다. 하지만 NSCoder는 추상 클래스로 프로그래머가 직접 NSCoder 클래스의 인스턴스를 만드는 일은 거의 없다, 대신에 이 클래스에서 구체화된 NSKeyedArchiverNSKeyedUnarchiver 클래스를 사용한다.

예제 1 – 입력된 내용을 저장하는 메모판

이 모든 이야기는 예제를 통해 확인하면 보다 쉽게 이해될 수 있다. Xcode에서 새로운 프로젝트를 만든다. 프로젝트 이름이야 다들 알아서 하시고 iOS Application에서 single view 기반의 프로젝트를 생성하자. 이는 Xcode의 버전에 따라서는 window-based 앱일 수도 있다.

프로젝트가 생성되면 스토리보드 (혹은 MainWindow) 파일이 있는데 여기에 텍스트뷰를 하나 집어 넣고 사용자가 내용을 입력할 수 있도록 속성창에서 editable에 체크를 해 준다.  또한 저장하는 액션을 호출하기 위한 버튼도 하나 달아준다. 버튼은 텍스트뷰 위에 달아도 되고 아래에 달아도 된다. 디자인은 각자 알아서 하면 될 거 같다.

이제 RootViewController.h 에는 다음의 내용에 다음 코드를 추가하자

#import <UIKit/UIKit.h>

@interface RootViewController : UIViewController
{
    NSString *dataFilePath;
}
@property (nonatomic, strong) IBOutlet UITextView *memo;
-(IBAction)saveData:(id)sender;
@end

dataFilePath는 데이터를 저장할 파일의 경로를 담는 변수가 될 것이며, memo는 인터페이스 빌더에서 추가한 텍스트뷰의 아울렛이다. 파일을 저장하고 인터페이스 빌더에서 해당 텍스트뷰에 아울렛을 지정해 준다. (지정이 끝나면 nib 파일을 꼭 저장하라)

이제 이 클래스의 구현부이다. 프로퍼티를 선언했으니 맨 먼저 해줘야 하는 일이 있겠지…  @implementation RootViewController 라고 써 있는 줄 바로 아래에 다음 문장을 추가한다.

@synthesize memo;

다음은 앱이 실행되어 뷰가 로드되었을 때 바로 파일을 저장할 경로를 준비해두는 작업을 해보자. 1viewDidLoad` 매서드를 찾아 다음 코드를 추가한다.

NSString *docsDir;
NSArray *dirPaths;
dirPaths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory
        , NSUserDomainMask, YES);
docsDir = [dirPaths objectAtIndex:0];
dataFilePath = [[NSString alloc] initWithString:[docsDir
stringByAppendingPathComponent:@"data.archive"]];

먼저 이 구문은 그냥 그대로 쓰는 거라고 외워두면 될 정도이다. 맨 마지막 줄의 파일 이름은 꼭 data.archive로 할 필요 없고 취향에 맞게 쓰면 된다. 이 코드는 앱의 하위 디렉토리 중에서 사용자 데이터 파일이 저장되는 Documents 디렉터리를 찾고 이 속에 파일이 저장될거라고 미리 파일의 경로를 생성해 두었다는 정도가 된다. 다행히 NSString은 파일 경로를 쉽게 다룰 수 있는 메서드를 이미 가지고 있으니 천만다행.

이제 실제 저장되는 부분을 보도록 하자.

-(IBAction)saveData:(id)sender
{
    [NSKeyedArchiver archiveRootObject:[memo text] toFile:dataFilePath];
}

좀 허무하리 만치 간단하지 않은가? NSKeyedArchiver 객체가 저장할 데이터를 아카이빙하고 파일에 저장하는 작업을 모두 처리해 준다.

이제 다시 인터페이스 빌더에서 저장 버튼을 뷰컨트롤러의 -saveData 메서드에 연결해준다.

뭔가 한가지 빠진게 있는데, 저장은 했다손 치더라도 그럼 앱이 다시 열렸을 때 저장된 내용을 복원해줘야 제맛 아니겠는가. 지금은 저장만 처리했지 데이터를 읽어오는 내용은 처리하지 않았다. 그럼 다시 RootViewController.m 에서 viewDidLoad 메서드의 끝 부분에 로딩에 관련된 코드를 추가하자.

로딩과 관련해서는 다음의 액션을 취한다.

  1. 먼저 데이터 파일 경로에 파일이 있는지 확인한다음,
  2. 파일이 있으면 파일을 읽어 들인다.
  3. 읽어들인 내용을 텍스트뷰에 옮겨준다.

코드는 다음과 같다.파일을 처리하기 위해서 NSFileManager를 사용한다.

// viewDidLoad의 마지막부분
NSFileManager *fileManager = [NSFileManager defaultManager];
if( [fileManager fileExistsAtPath:dataFilePath])
{
    NSString *memoData = [NSKeyedUnarchiver unarchiveObjectWithFile:dataFilePath];
    memo.text = memoData;
}

사실 여기까지하면 끝인데, 한 가지만 더 추가하자. 만약 텍스트 뷰 아래에 저장 버튼을 추가했다면, 키보드가 올라오는 바람에 저장 버튼을 누를 수 없는 경우가 있을 수 있는데, 뷰의 배경을 클릭해서 키보드를 닫도록 하자. 역시 뷰컨트롤러의 구현부에 추가한다.


-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    if( [memo isFirstResponder] ){
        [memo resignFirstResponder];
    }
}

끝이다. 빌드하고 실행해본다. 에뮬레이터에서 앱을 종료한 후 다시 실행해보아도 입력했던 내용이 여전히 남아 있는 것을 확인할 수 있을 것이다.

그런데 이 예제만 만들고 나면 왠지 좀 속는 기분이 든다. 만약 뷰에서 다루는 데이터가 여러 종류이고, 이를 함께 저장하려면 어떻게 하는가?  그럼 이 메모장에서 제목란을 추가해서 한 번 수정해 보도록 하자.

인터페이스 빌더에서 루트 뷰 컨트롤러의 텍스트 뷰 크기를 좀 조정해서 빈칸을 만든 다음, 여기에 텍스트 필드를 하나 추가한다. 그리고 뷰컨트롤러의 헤더에 이 텍스트 필드에 대한 아울렛을 지정하는 코드를 만들자.

@property (nonatomic, strong) IBOutlet UITextField *title;

헤더 파일을 저장한 다음, 인터페이스 빌더에서 이 아울렛을 실제 텍스트 필드와 연결한 후 저장한다. 다시 구현부로 간다. 추가한 property에 대해 synthesize 구문을 추가해준다.

@synthesize title;

이제 -saveData: 메서드를 바꾼다. title과 memo의 내용을 한 번에 저장할 것이기 때문에 NSString을 아카이빙하는 것이 아니라 NSDictionary로 만들어서 저장한다.

-(IBAction)saveData:(id)sender
{
    NSMutableDictionary *dataDictionary = [[NSMutableDictionary alloc]
        initWithObjectsAndKeys:title.text,@"title",memo.text,@"memo", nil];
    [NSKeyedArchiver archiveRootObject:dataDictionary toFile:dataFilePath];
}

저장하는 방법을 바꿨으니, 불러오는 방법도 바꿔야겠지. -viewDidLoad 의 마지막 부분을 아래와 같이 바꾸면 된다. NSKeyedUnarchiver를 사용하여 NSDictionary를 복원한다음, 각 키 값을 사용하여 title, memo의 내용을 복원해주면 된다.

 
if([filemanager fileExistsAtPath:dataFilePath])
{
    NSDictionary *dataDictionary;
    dataDictionary = [NSKeyedUnarchiver unarchiveObjectWithFile:dataFilePath];
    memo.text = [dataDictionary objectForKey:@"memo"];
    title.text = [dataDictionary objectForKey:@"title"];
}

자, 이래도 아카이빙이 어려운가?   다음 글에서는 메모 한 개가 아니라 여러 개를 만들 수 있는 앱을 어떻게 만들 것인지 알아보도록 하겠다. 긴 글 읽으시느라 고생많았다.

이어지는 글 : 저장이 가능한 간단 메모장 2 (1/2)

20110828 :: iOS5 : Empty Application에서 Storyborad 생성하기

잊기전에 하는 메모

  1. Storyboard

iOS5에 추가된 기능으로, 하나의 파일에서 애플리케이션의 모든 혹은 일부의 UI를 디자인하고 각 뷰의 상관관계 및 네비게이션을 설정할 수 있는 도구.

  1. Empty Application

AppDelegate외에는 다른 클래스는 전혀 만들어지지 않은 프로젝트. 보통 RootViewController 라는  UIViewController 를 생성해서 메인 화면을 작성하게 된다.

  1. Empty Application에서 Storyborad  기반 앱으로 발전시켜가기
  1. 먼저 빈 스토리보드를 하나 생성한다. 이름은 주고 싶은대로.
  2. 프로젝트 세팅에서 타겟 세팅값 중 Main Storyborad File Base Name 속성을 새로 만들어 지정해준다.
  3.  AppDelegate.m 파일에는 앱이 런칭되었을 때 main window를 생성하는 코드가 있다. 이를 제거한다. 
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:
                                                    (NSDictionary *)launchOptions
{
    return YES;
}
  1. 스토리보드를 프로젝트에 연결하면 자동으로 윈도우는 생성이 된다. 최초 뷰를 생성하기 위해서는 인터페이스 빌더에서 View Controller 객체를 삽입하면 된다.

20110702 :: 비어있는 프로젝트에서 첫 화면 띄우기

안타깝게도 블로그 글이 아이폰 앱 개발 관련 글로 채워지고 있지만…  메모 차원에서…

  1. 프로젝트 생성 : EmptyApp 이라는 이름으로 프로젝트 생성
  2. Storyboard 생성 : EmptyApp.stroyboad라는 이름으로 빈 스토리보드 생성
  3. 생성된 스토리보드에서 첫번째 뷰 컨트롤러 생성. initial scene 인지는 속성창에서 체크되어 있는지 여부로 알 수 있다.
  4. 앱 델리게이트에서 다음 코드로 앱을 초기화 한다. 일부 코드는 이미 생성되어 있음
-(BOOL)application:(UIApplication *)application didFinishedLaunchingWithOptions:(NSDictionary *)launchingOptions{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainscreen] bounds]];

    UIStoryboard *myStoryboard = [UIStroyboard storybordWithName:@"EmptyApp" fromBundle:[NSBundle mainBundle]];
    UIViewController *rootViewController = [myStoryboard instantiateInitialViewController];

    [self.window addSubView:rootViewController.view];

    [self.window makeKeyAndVisible];
    return YES;

}

이외에도, plist 파일을 통해 main storyboard 파일을 지정해버리는 방법도 있다.[1. 다른 템플릿에는 위의 코드가 없어서 찾아본 내용. 7/2 추가]