CGLayer를 사용한 핑거 드로잉 구현 (Objective-C)

코어 그래픽(Core Graphics)은 저수준의 드로잉 명령 API들을 통해서 화면이나 비트맵이미지, PDF 등에 시각적 요소를 그릴 수 있게 하는 프레임워크이다. 예전에는 Quartz, CoreGraphics라는 이름으로 분리되어 있었는데 iOS5 부터 UIKit의 일부로 완전히 편입되었다. 간단한 모양의 시각적 오브제를 표현하기 위해 비트맵 이미지를 사용하는 것보다 런타임에 오브제를 빠르게 그리고, 이를 재사용할 수 있게 하는 등의 기능을 제공한다. 실제로 많은 앱들이 현재에도 코어 그래픽을 사용해서 UI를 표현하는 경우가 많이 있다. 이번 글에서는 코어 그래픽 API를 사용해서 손가락으로 화면에 그림을 그리는 간단한 캔버스 앱을 구현하는 방법을 살펴보기로 하겠다.

코어 그래픽을 사용할 때에는 이 프레임워크의 핵심 객체인 그래픽 컨텍스트에 대한 이해가 필요하다. 그래픽 컨텍스트는 개념상, 가상의 캔버스라 생각하면 된다. 우리는 그래픽 컨텍스트라는 이 가상의 캔버스에 그림을 그리게 되고, 쿼츠 엔진은 이 가상의 캔버스에 그려진 그림을 필요한 출력으로 가져다 렌더링한다. 따라서 그래픽 컨텍스트는 장치 독립적인 성격을 가지며, 하나의 그래픽 컨텍스트는 다른 장치를 위한 그래픽으로 쉽게 전환이 가능하다. 따라서 그래픽 컨텍스트에 적용된 그래픽은 아이폰 및 아이패드용 화면 출력 뿐 아니라, 인쇄나 PDF를 만들기도 쉽게 지원된다. 실제로 macOS를 보면 모든 뷰는 PDF로 변환이 가능하고, 화면에 그려지는 모든 것이 PDF인 동시에 PNG일 수 있는데, 이것은 macOS의 드로잉 체계가 컨텍스트라는 개념을 중심으로 장치독립적으로 추상화되어 있기 때문에 가능한 것이다.

모든 화면에 출력되는 모든 뷰는 콘텐츠를 시각적으로 표현하는 도구이며, 그 콘텐츠를 뷰에 제공해주는 주체가 바로 그래픽 컨텍스트이다. 가상의 캔버스인 컨텍스트에 코어 그래픽 API를 사용하여 그림을 그리면 이 데이터가 그래픽 버퍼로 덤프되고, 그 결과 이미지가 화면에 뿌려지게 된다. 보통 특정한 뷰에 이렇게 그림을 그리기 위해서는 해당 뷰에서 “현재 컨텍스트”를 얻고 여기에 그림을 그리면 된다. 물론 컨텍스트는 별도로 생성할 수 있다. 별도로 생성한 컨텍스트에 그림을 그리는 것은 일종의 그래픽 데이터를 메모리 내에 준비하고 있는 것이 된다.

만약 추가적으로 생성한 그래픽 컨텍스트와 CGLayer를 결합하면, 컨텍스트의 데이터가 레이어의 콘텐츠를 제공하게 되고, 다시 이 레이어는 다른 컨텍스트에서 일종의 스탬프처럼 찍어서 사용할 수 있다. 이런식으로 직접 화면에 그리는 것이 아니라, 컨텍스트에 미리 그림을 그려놓고 이것을 재활용하여 반복적인 문양을 그리는 것을 (왜냐면 CGLayer는 재사용할 때 캐시된다!) 오프스크린 드로잉이라고 한다. 특히 오프스크린 드로잉은 백그라운드 스레드에서 처리가 가능하다는 장점이 있다. (iOS에서는 화면에 무언가를 그리기 위해서는 반드시 메인스레드에서 작업해야 한다.) 따라서 워커 스레드에서 미리 콘텐츠를 제작해놓고 메인스레드에서는 최종 결과물만 업데이트하는 식으로 처리하여 화면의 빠른 드로잉이 가능하게 할 수 도 있다.

플로우

핑거 드로잉은 말 그대로 화면에 손가락을 터치하고, 손가락이 터치해서 움직이는 경로를 따라 화면에 선이나 무늬를 그려넣는 것을 말한다. 즉, 가장 기본적인 인터랙티브 드로잉 방법이다. 이 핑거 드로잉을 지원하는 캔버스 뷰를 작성해보도록 하겠다. 기본 개념은 다음과 같다.

┌───CanvasView───┐                  ┌─Offscreen Layer─┐
│  View'sContext │                  │ Layer's Context │  
│                │    touch --->    │                 │
│                │                  │                 │
│                │                  │                 │
│                │       Draw       │                 │
└────────────────┘ <--------------- └─────────────────┘
  1. 뷰와 크기가 똑같은 레이어를 하나 준비한다.
  2. 손가락이 움직이는 궤적은 레이어에 그려진다.
  3. 레이어에 그림이 그려진 후에는 뷰에 레이어를 그린다.

실제로 손가락이 움직이는 궤적은 눈에 보이지 않는 캔버스에 그림을 그리는 것이다. 그리고 이렇게 그려진 데이터를 뷰에 언제 그리느냐에 따라서 손가락을 움직이는 사이사이에 선이 그려지게 할 것인지, 아니면 손가락을 떼는 시점에 그림이 나타나게 할 것인지를 결정할 수도 있다.

프로젝트 시작

새 프로젝트를 하나 만든다. 어차피 UIView 클래스를 새로 하나 만드는 것이 사실 구현의 전부이므로, Single View App으로 시작한다. 프로젝트를 생성하였으면, 새 파일을 추가한다. Objective-C 을 언어로 정하고, 클래스는  UIView를 선택한다. 이름은 CanvasView 정도가 좋을 것 같다.

앱 실행시에 해당 뷰가 전면에 표시되도록 이 캔버스 뷰가 들어갈 뷰 컨트롤러의 파일에서 viewDidLoad를 다음과 같이 수정해서 루트 뷰에 캔버스뷰를 추가한다. (혹은 UI빌더에서 UI뷰를 하나 삽입하고, 그 클래스를 CanvasView로 선택해도 된다.)

viewcontroller.h 수정하기

기본으로 세팅된 메인 뷰가 로드되면 캔버스뷰를 만들어서 자기 위에 얹도록 코드를 작성한다. 만약 스토리보드에서 메인 뷰의 클래스를 Canvas로 변경했다면 이 코드는 작성하지 않는다.

#import "CanvasView"
/* ... */
-(void)viewDidLoad {
    CanvasView *canvas = [[CanvasView alloc] 
                         initWithFrame:self.view.frame];
    [self.view addSubView:canvas];
}

캔버스뷰의 인스턴스 변수 정의

CanvasView의 헤더를 작성하자. 두 개의 인스턴스 변수를 선언한다. 뷰 내에서 오프스크린 드로잉을 담당할 레이어를 위한 CGLayerRef 변수와, 그 레이어에 그림을 그릴 수 있는 CGContext 타입 변수를 선언한다. 이 두 변수는 OpaqueType 이며, Objective-C 클래스가 아니므로 * 를 붙이지 않음에 유의하자.

@import UIKit;

@interface CanvasView: UIView
{
  CGContextRef layerContext;
  CGLayerRef drawingLayer;
} @end

초기화

초기화작업은 통상의 UIView의 초기화 프로세스를 따른 후, 두 개의 인스턴스 변수에 대한 초기화를 수행한다. 이 때의 순서는 다음과 같다.

  1. 비트맵 컨텍스트를 하나 생성한다.
  2. 1의 컨텍스트를 기반으로 CGLayer를 생성한다. 사실, 1에서 컨텍스트를 만들지 않고 참조 컨텍스트로는 NULL을 넣어도 상관없다. 하지만 넣어주는 경우에 조금 더 최적화된다고 한다.
  3. 2에서 생성한 레이어로부터 실제 레이어의 콘텐츠를 담을 컨텍스를 얻어서, 이를 drawingContext 값으로 대입한다.
  4. drawingContext를 설정한다. 선의 색이나, 굵기, 끝모양 등의 정보를 지정할 수 있다.
#import "CanvasView.h"

@implementation CanvasView
-(id) initWithFrame: (CGRect)frame
{
  self = [super initWithFrame: frame];
  if (self) {
    [self initContext];
  }
}

-(id) initWithCoder: (NSCoder*)aDecoder
{
  self = [super initWithCoder: aDecoder];
  [self initContext];
}

-(void) initContext {
  CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
  CGContextRef ctx = CGBitmapContextCreate(
                  NULL, 
                  10, 
                  10, 
                  8, 
                  0, 
                  cs, 
                  kCGBitmapAlphaPremulipliedLast);
  // 각각의 파라미터는 다음과 같은 의미이다.
  // 1 : NULL - 비트맵을 저장할 메모리 블럭, NULL을 넘겨주면 이 함수가 자동으로 할당한다.
  // 2, 3 : 10, 10 은 컨텍스트의 픽셀 크기이다. 
  // 4 : 8 - 컴포넌트당 비트. RGBA가 각각 8비트, 총 32비트 트루컬러를 사용할 것이다.
  // 5 : 0 - 한 줄당 바이트. 픽셀당 4바이트를 사용하고 총 10픽셀 폭이니 40이 될 것이다. 이는 1번 파라미터가 NULL인 경우
  //         0을 넘겨서 자동계산하게 할 수 있다. 
  // 6 : cs - 컬러스페이스
  // 7 : 비트맵정보 - 미리 정의된 CGImageAlphaInfo 상수를 사용한다.

  // 레이어와 컨텍스트 생성/초기화.
  drawingLayer = CGLayerCreateWithContext(ctx, self.bounds.size, NULL);
  drawingContext = CGLayerGetContext(drawingLayer);
  // 컨텍스트에 그림을 그리는 방법을 세팅한다.
  CGContextSetStrokeColorWithColor(drawingContext, [[UIColor redColor] CGColor]);
  CGContextSetLineWidth(drawingContext, 4.8f);

  // 참조용으로 생성한 객체들을 정리한다. 
  CGContextRelease(ctx);
  CGColorSpaceRelease(cs);
}

이 코드에서 주목해야 할 점은 맨 처음 비트맵 컨텍스트를 생성할 때, 사이즈를 화면 크기가 아니라 제멋대로 주었다는 점이다. 이 컨텍스트를 기반으로 레이어를 만들 때, 레이어만 뷰의 크기와 일치시켰다. CGLayerCreateWithContext() 함수에서 흔히 잘못알고 있는 점은 이 함수에서 넘겨지는 그래픽 컨텍스트 객체는 레이어에 그려지는 그림과는 실제 무관할 수 있다는 점이다. 실제로 비트맵 컨텍스트를 만들지 않고 NULL을 전달하여도 코드는 정상적으로 동작한다. 그렇다면 이는 왜 필요한 것일까?

그래픽 컨텍스트는 장치독립적인 페이지이며, 이 페이지는 앱의 윈도에 그려지거나, 프린터로 출력되거나 혹은 비트맵 이미지로 고정될 수 있다. 이 때 각각의 출력 디바이스에 따라서 다른 정보들이 사용되고, 이는 내부적인 타입이 구분되어 사용되는 것으로 이해할 수 있다. CGLayerCreateWinContext()함수에서 전달받는 컨텍스트 인자는 CGLayer를 생성할 때 캔버스로 사용할 컨텍스트를 받는 것이 아니다. 레이어를 생성할 때, 레이어의 컨텍스트는 별도로 생성되는데, 이 때 인자로 전달받은 컨텍스트의 속성을 참조하여 생성되는 컨텍스트를 최적화한다. (실제로 이를 NULL로 전달하는 것보다, 이렇게 간단하게 만들어서 전달하는 경우, 드로잉 성능이 더 좋다.) 즉, ctx != CGLayerGetContext(drawingLayer) 이다.

 

터치 동작 구현

UIView에서 터치가 움직일 때, 뷰는 touchesMoved:withEvent 메시지를 받는다. 이 메소드를 오버라이딩하여 움직인 만큼 부분에 선을 그려넣도록 한다. 선을 그려넣는 작업은 현재 뷰의 컨텍스트가 아닌 drawingContext이다. 여기에 콘텐츠를 그려넣은 다음에 뷰에는 레이어를 그려넣으면 된다.

- (void)touchesMoved:(NSSet<NSTouch *>*)touches withEvent:(UIEvent *)event
{
  CGPoint lastTouch, currentTouch;
  UITouch *touch = [touches anyObject];
  lastTouch = [touch previousLocationInView: self];
  currentTount = [touch locatioinInView: self];

  // 선을 그리자. 
  // 이전 위치로 이동후 현재 위치로 선을 추가한다. 
  CGContextBeginPath(drawingContext);
  CGContextMoveToPoint(drawingContext, lastTouch.x, lastTouch.y);
  CGContextAddLineToPoint(drawingContext, currentTouch.x, currentTouch.y);
  CGContextStrokePath(layerContext);

  // 그려진 레이어를 뷰에 반영하기 위해 뷰 업데이트를 스케줄링한다.
  [self setNeedsDisplay];
} 

이상의 구현에서 특별한 점은 없다. 여기서는 터치가 조금씩 움직이는 주기마다 컨텍스트에 선을 추가하고, 뷰를 업데이트하도록 한다. 만약 손가락을 뗐을 때만 뷰가 업데이트되도록 하려면 [self setNeedsDisplay];를 touchesEnded:withEvent:에서 호출하도록 한다.

뷰 업데이트

setNeedsDisplay 메시지를 받으면 뷰는 자신의 상태가 유효하지 않다는 것을 감지하고 뷰 영역을 새로 그리려고 시도한다. 이는 drawRect: 메시지를 호출하여 그리게 된다. 이미 지금까지 그려놓은 모든 페인팅은 drawingContext에 남아있고, 이를 뷰에 찍기 위해서는 drawingLayer를 그려주면 된다. 여기서는 뷰의 현재 컨텍스트를 이용해서 레이어를 그리면 된다.

- (void)drawRect: (CGRect) rect
{
  [super drawRect:rect];
  CGContext ctx = UIGraphicsGetCurrentContext();
  CGContextDrawLayerInRect(ctx, self.bounds, drawingLayer);
}

정리

이제 모든 소스 내용을 검토해보자.

  1. 앱이 시작되면 메인 뷰에 캔버스 뷰 인스턴스를 만들어서 붙인다.
  2. 캔버스 뷰는 생성되면 자신의 크기와 같은 오프스크린 레이어와 레이어에 그림을 그릴 컨텍스트를 생성한다.
  3. 뷰가 터치를 받고, 터치가 움직이면 이 궤적을 따라서 선을 만들 수 있고, 이 선은 레이어의 컨텍스트에 추가된다.
  4. 선을 그릴 때마다 뷰는 업데이트 요청을 받고, 컨텍스트의 비트맵 데이터가 투영된 레이어가 뷰에 찍힌다.

조금 더 깊이

코어 그래픽 컨텍스트는 장치 독립적인 가상 캔버스라고 했다. 그리고 어떤 장치를 통해서 표현되느냐에 따라서 비트맵 형식일 수도 있고, PDF 형식일수도 있다. CGLayerCreateWithContext() 에서 넘겨지는 컨텍스트는 실제로 생성된 레이어의 컨텍스트를 특정 타입으로 최적화하기 위해 필요하며, 생성된 레이어에 대해 CGLayerGetContext()로 얻게되는 컨텍스트와는 동일할 수도, 그렇지 않을 수도 있다. 따라서 이 둘이 같은 것이라는 어떠한 가정도 해서는 안된다. 실제로 drawingContext를 비트맵 컨텍스트로 생성한 후, 이 컨텍스트를 참조로 레이어를 만든다면 컨텍스트에 그린 그림이 레이어에 전혀 반영되지 않을 것이다.

 

20120103 :: [팁] 프로토타입 셀을 서브클래싱하기

프로토타입 셀에 라벨 등의 서브 뷰를 삽입하면 각각의 서브뷰에 tag 값을 주어 -viewWithTag: 메소드를 통해 접근이 가능하다. 하지만 태그를 일일이 외우는 것은 직관적이지 못하므로 (물론 편리하긴하지만) 셀의 하위 구조가 복잡한 경우에는 그닥 즐겨 쓸만한 일이 아니다. 이를 아울렛을 통해 접근하고 제어할 수 있다면 좀 더 편리할 것이다.

이런 경우에는 프로토타입 셀을 서브 클래싱할 수 있다. 즉 UITableViewCell 클래스의 하위 클래스를 만들고 이를 IB에서 명시해주면 된다. 먼저 스토리보드에서 테이블 뷰 안에 있는 프로토타입 셀에 이것 저것 넣고 싶은 레이블이나 이미지뷰등을 넣는다.

원하는 서브 뷰에 대해서는 아울렛 이름을 생각해 본 다음, 다음과 같이 클래스를 하나 추가한다.

//SampleCell.h

@interface SampleCell : UITableViewCell

@property (nonatomic, strong) IBOutlet UILabel *nameLabel;
@property (nonatomic, strong) IBOutlet UILabel *gameLabel;
@property (nonatomic, strong) IBOutlet UIImageView *ratingImageView;
@end

//SampleCell.m

#import “SampleCell.h”
@implemetation SampleCell
@synthesize nameLabel, gameLabel, ratingImageView;
@end

스토리보드(혹은 xib)에서 프로토타입 셀을 선택해 클래스 속성을 SampleCell로 변경한다. 왼쪽 객체 트리구조 리스트에서 셀의 각 하위뷰로 드래그하여 아울렛을 연결해준다. 이 작업이 완료되면 테이블 뷰는 reuse identifier에 대해 sampleCell 의 인스턴스를 반환하게 된다. (이 때, 중요한 것은 테이블뷰 셀에 아울렛을 끌어와야 한다는 점이다. 테이블 뷰나 뷰 컨트롤러에 이를 끌어와서는 곤란하다!!!)

프로토타입 셀의 클래스 이름과 Reuse Identifier 간에는 어떤 상관 관계도 없으므로 같은 이름을 주지 않아도 상관없다.

20120102 :: [iOS] 저장이 가능한 간단 메모장 3 (코어데이터)

해당 포스트의 코드가 너무 부끄러운 수준으로 디자인이 잘못돼 있어서 새롭게 작성한 글이 있으니 이 글을 참고해주세요.

이미 세 개의 포스팅(관련글 1, 관련글 2, 관련글 3)을 통해 간단한 메모장 앱을 만드는 방법을 살펴보았는데, 이번에는 완전히 똑같은 앱을 코어데이터를 사용하여 생성하는 방법에 대해 살펴보도록 하겠다. 이 시리즈의 맨 처음에 코어데이터에 대해 언급하면서 초보자에게는 좀 많이 어렵다고 이야기한 바 있는데, 이는 실제로 코어데이터가 사용하기 어려운 프레임워크라는 의미라기보다는 코어데이터를 실제로 사용하기 위해서 알고 있어야 하는 배경 지식이 상당히 많다는 의미라고 보는 것이 정확할 듯 하다. 20120102 :: [iOS] 저장이 가능한 간단 메모장 3 (코어데이터) 더보기

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

이번 시간에는 지난 글에 이어 앱을 작성된 메모를 테이블뷰에 보여주고, 이를 편집하고 삭제하는 기능을 추가해 보도록 하겠다. 사실 메모장 만들기의 핵심 파일에 데이터를 저장하는 것이고 맨 처음 글과의 차이점은 여러 개의 메모를 객체로 만들어서 파일에 저장하는 방법을 서술했다. 마지막 글은 앱의 모양새를 조금 다듬는 정도가 되겠지만, 테이블뷰 사용에 익숙하지 않은 초보자에게는 좋은 읽을 거리가 될 수 있도록 하겠다.

테이블 뷰

테이블 뷰는 iOS에서 주로 목록을 만들 때 많이 사용하게 된다. 가장 쉬운 예로는 연락처 앱의 메인 UI를 생각할 수 있으며, 조금 다른 모양새이기는 하나 설정 앱도 테이블 뷰를 기반으로 하고 있다. 테이블 뷰는 여러 개의 셀을 나열하는 조금 독특한 형태의 뷰로, 이 테이블뷰를 구현하기 위해 테이블 뷰 컨트롤러는 2가지의 프로토콜을 따라야 한다. 그것은 각각 UITableViewDelegateUITableViewDataSource이다.

데이터소스는 테이블뷰의 각 셀에 표시되는 내용을 만들어주는 역할을 한다. 즉 테이블 뷰에 섹션이 몇개있고, 각 섹션에는 몇 개의 셀이 있으며, 각 셀에는 어떤 내용이 들어가야 하는지를 알려주는 것이다. 테이블 뷰는 자신을 그려야할 때 데이터소스로 지정된 객체에 일련의 메시지를 보내어 자신이 렌더링 되어야 하는 모양을 알게 된다. (그리고 이 때 보내는 메시지들은 UITableViewDataSource에 선언되어 있다.)

테이블뷰 델리게이트는 테이블 뷰의 각 셀을 탭하거나 지우거나 편집하는 등의 액션과 관련이 있다. 단지 테이블뷰가 내용만을 보여주고 별다른 사용자 상호작용을 하지 않는 경우에는 특별히 구현해야 하는 필요가 없다.

이제, 테이블 뷰 컨트롤러로 만들어진 RootViewController를 수정하여 저장된 내용을 보여주고, 셀을 탭하여 기존 내용을 편집하는 방법에 대해 살펴보도록 하겠다.

인터페이스

실제 데이터는 앱 델리게이트가 가지고 있으며, 테이블 뷰 컨트롤러에 필요한 것은 각 셀의 내용을 담는 배열하나 뿐이다. 따라서 인터페이스 부분은 다음과 같이 만들어진다.

#import <UIKit/UIKit.h>
#import "AppDelegate.h"
@interface RootViewController : UITableViewController
{
    NSArray *itemListArray;
    AppDelegate *appDelegate;
}
@end

구현부

테이블 뷰 컨트롤러의 구현 부 중 데이터 소스를 먼저 살펴보자. 데이터 소스는 테이블 뷰를 새로 그릴 때마다 “섹션은 몇 개”이며 “각 섹션에는 셀이 몇 개” 있고, “각 셀은 이러이러하다”는 내용을 테이블 뷰에게 알려주는 메소드들이다. 따라서 이 세가지 메소드는 필수적으로 구현해야 하며 각 메소드의 이름은 다음과 같다.

  • – (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
  • – (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
  • – (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

이 중 마지막 tableView:cellForRowAtIndexPath: 가 가장 중요한데, 테이블 뷰는 알려진 셀의 개수만큼 루프를 돌면서 이 메소드를 호출하게 되고, 매 호출시 마다 idnexPath에 담긴 정보에 따라 해당 셀을 반환받게 된다.  또 한가지 중요한 것은 테이블 뷰는 아주아주 큰 크기를 가질 수 있는데 예를 들어 5000개 정도의 길이를 갖는 테이블 뷰가 있다고 하면 이 뷰를 채울 테이블 뷰 셀 역시 5000개가 필요하나, 메모리에 큰 부담을 주게 되므로 테이블 뷰는 이미 만들어진 셀을 재활용하게 된다. 따라서 각 셀은 Reuse Identifier를 설정해 주어 이에 맞는 셀을 사용하도록 한다.

스토리보드에서 테이블 뷰의 셀을 선택하고 Reuse Identifier에 Cell 이라고 입력한다. 또한 셀의 스타일은 기본적으로 Custom으로 되어 있는데 따로 디자인을 변경할 것이 아니므로 Basic으로 변경한다. (Basic 은 텍스트라벨 하나만을 표시하는 기본 셀이다.)

다시 RootViewController.m 파일로 돌아와서 각 부를 구현한다. 테이블 뷰 컨트롤러 템플릿으로부터 만들게 되니 아마 필요한 메서드들은 기본적으로 타이핑이 되어 있다. 먼저 뷰가 로딩되었을 때이다.

-(void)viewDidLoad
{
    [super viewDidLoad];
    appDelegate = [[UIApplication sharedApplication] delegate];
    itemListArray = [[NSArray alloc] init];
}

우선 섹션은 1개만 사용하고, 셀의 개수는 메모의 개수와 같다.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    itemListArray = [[appDelegate memoListArray] copy];
    return [itemListArray count];
}

각 셀을 표시하도록 한다.

- (UITableViewCell *)tableView:(UITableView *)tableView 
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell"; //스토리보드에서 입력한 reuse identifier와 동일한 문자열
    UITableViewCell *cell = [tableView deque.... :cellIdentifier];
    // Cell의 내용을 정의
    [[cell textLabel] setText:[[itemListArray objectAtIndex:indexPath.row] title]];

    return cell;
}

상세 뷰에서 새 메모를 추가한 경우, 이를 테이블 뷰에 반영하기 위해서는 뷰가 보여질 때 테이블 뷰의 데이터를 갱신해야 한다.

-(void)viewWillAppear:(BOOL)animated
{
    [self.tableView reloadData];
}

이제 앱을 빌드하고 실행하면 기존에 생성했던 메모들이 표시되고, 새 메모를 작성하고 돌아오면 신규 메모가 리스트의 맨 상단에 표시되는 것을 볼 수 있다.

이번에는 기존 메모를 편집하는 부분이다.  먼저 스토리보드에서 테이블뷰의 셀을 선택해, 이를 디테일뷰와 연결한다. 연결 유형은 push로 한다. 이제 셀을 탭하면 디테일뷰로 이동하게 된다. 그런데 셀을 탭해도 신규 메모를 만드는 화면이 뜨게 되는데 이는 앱 델리게이트의 메모인덱스가 계속 -1 이기 때문이다. 이를 변경하는 절차가 있어야 한다.

기본적으로 테이블 뷰 델리게이트에는 셀을 선택했을 때 동작을 정의하는 메소드가 있지만, 우리는 스토리보드에서 segue를 통해 이동하므로 선택이 되기 직전에 인덱스를 바꾸는 작업을 해야 한다. 따라서 기존에 타이핑이 되어있는 메서드인 tableView:didSelectRowAtIndexPath: 를 지우고 새로운 메소드를 정의한다. 즉 선택이 되기 직전에 호출되는 메소드이다.

-(NSIndexPath *)tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [appDelegate setMemoIndex:indexPath.row];
    return indexPath;
}

이제 기존 메모를 선택하면 해당 메모의 내용이 표시되는 것을 앱을 실행하여 확인할 수 있다.

다음은 셀을 지우는 부분이다. 이 중 많은 내용은 이미 타이핑이 되어 있을 것이다.

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    if(editingStyle == UITableViewCellEditingStyleDelete){
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        [[appDelegate memoListArray] removeObjectAtIndex:indexPath.row];
        [appDelegate saveData];
    }
}

여기까지 우리는 간단한 메모를 만들고 이를 편집/삭제하여 외부파일로 저장하는 앱을 만드는 방법을 살펴보았다. 언제가 될지는 모르겠지만 지금까지 만들어본 내용을 코어데이터를 사용하여 만드는 방법도 한 번 포스팅할 생각인데, 역시나 연재라는 건 부담이 크고 나는 너무 게으르기 때문에 그 언제가 언제가 될지는 모르겠다. 긴 글 읽어주셔서 감사하다.

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]);
}