- Paper는 Apple 플랫폼에서 TextKit 1 기반
TextView편집기로 동작하며,NSTextStorage,NSTextContainer,NSLayoutManager,TextView,ScrollView가 함께 편집 경험을 구성함 NSAttributedString은 문자열과 범위별 속성(attribute) 으로 리치 텍스트를 표현하고, Paper는 Markdown 구조용 메타 속성과 화면 표시용 스타일 속성을 분리함- 스타일 갱신은 문서 열기, 텍스트 변경, 설정 변경에 따라 달라지며, 레이아웃에 영향을 주는 속성은 트랜잭션으로 묶고 장식 속성은 레이아웃 재계산 없이 갱신함
- 입력 성능은 Markdown 특수 문자 주변 편집 여부를 보고 문단 전체 재파싱을 피하는 방식으로 개선되지만, 코드 블록처럼 문서 전체 스타일에 영향을 줄 수 있는 구조는 예외 처리가 필요함
- 복사·붙여넣기는 UTI와 pasteboard의 여러 데이터 표현 위에서 동작하며, Markdown처럼 Apple이 정의하지 않은 세미공개 UTI는 앱 간 우선순위 불일치로 RTF 변환과 UX 문제가 생길 수 있음
Apple TextView 편집 스택
- Paper는 오래된 TextKit 1 프레임워크 위에 만들어져 있으며, 같은 개념과 추상화는 TextKit 2에도 그대로 있거나 더 나은 API 아래 존재함
NSTextView와UITextView는 차이가 있지만 API가 충분히 유사해 하나의TextView계열로 다룰 수 있음TextView는 OS 릴리스마다 복잡해지는 큰 컴포넌트이며, TextEdit 앱은 거의 단일TextView로 구성됨-
편집기를 이루는 핵심 계층
NSTextStorage- 원본 텍스트 문자열을 저장함
- 텍스트 범위에 붙은 문자열-값 쌍의 속성을 저장함
- 텍스트와 속성 변경 이벤트를 발생시킴
NSTextContainer- 글리프가 들어갈 영역의 모양과 크기를 정의함
- 대부분 사각형이지만 임의의 모양도 가능함
NSLayoutManagerNSTextStorage의 속성 범위를 보고 글리프 크기와 간격을 계산함- 폰트에서 벡터 글리프를 추출하고, 문자 하나를 하나 이상의 글리프로 변환함
- 각 줄의 시작·끝, 줄 수, 전체 텍스트 높이를 계산함
TextViewNSLayoutManager가 만든 글리프 레이아웃을 그림- 뷰 높이를 현재 텍스트 레이아웃 높이와 동기화함
- 입력, 선택 영역, 캐럿, 입력 속성,
textContainerInset, 받아쓰기, 복사·붙여넣기, 맞춤법 검사 등을 관리함
ScrollViewTextView의 보이는 부분, 스크롤, 스크롤바, 줌을 관리함TextView의textContainerInset과 별도로contentInset을 가질 수 있음
-
AppKit과 UIKit의 스크롤 차이
- AppKit에서는
NSScrollView가NSClipView와 두 개의NSScroller를 포함하고,NSClipView가NSTextView를 담아 여러 클래스가 스크롤 동작을 함께 만듦 - UIKit에서는
UITextView가UIScrollView를 상속하므로 스크롤 로직까지 직접 포함함 - 캐럿이
contentInset으로 제한된 보이는 영역 밖으로 이동하면UITextView가 자동 스크롤함 - iOS 편집기에서 캐럿이 키보드 뒤로 가면 다음 줄로 스크롤되는 동작은 하단
contentInset이 현재 키보드 높이로 동적으로 설정되기 때문임
- AppKit에서는
NSAttributedString과 속성 조회
NSAttributedString은 Apple 프레임워크의 리치 텍스트 편집 기반이며 두 요소로 구성됨- 일반 텍스트 문자열
- 문자열 안의 텍스트 범위에 붙은 문자열-값 속성 쌍
- 속성은 주로 스타일링에 쓰이지만, 개발자가 필요한 임의의 문자열-값 쌍도 붙일 수 있음
NSRange는location과length로 구성되며,NSMakeRange(10, 5)는 위치 10에서 시작하는 5글자 범위, 즉 10~14 위치를 뜻함- 같은 위치에 같은 속성이 여러 범위로 지정되면 마지막에 적용한 범위가 우선함
-
특정 위치의 속성과 범위 확인
- 특정 위치의 속성 값은
attribute:atIndex:effectiveRange:로 확인함 - 반환값이
nil이면 해당 속성이 없고, 반환값이 있으면 예시 기준NSFont또는UIFont객체 같은 속성 값임 - 마지막 인자로
NSRange포인터를 넘기면 같은 값의 속성이 이어지는 범위 또는 속성이 없는 공백 범위를 받을 수 있음 - 문서화상
effectiveRange는 반드시 최대 범위가 아니며 구현에 의존함 - 보장된 최대 범위가 필요하면
attribute:atIndex:longestEffectiveRange:inRange:를 사용해야 함 - 특정 위치의 모든 속성 조합과 그 조합이 이어지는 범위를 얻는 메서드도 있음
- 범위 안의 속성 순회에는
enumerateAttribute:inRange:options:usingBlock:을 사용할 수 있음
- 특정 위치의 속성 값은
Paper의 Markdown 스타일링 파이프라인
- Paper의 스타일링은 텍스트 범위에 프레임워크가 정의한 특수 속성을 적용하는 방식임
- 스타일링 전에 텍스트 구조를 식별하기 위해 커스텀 메타 속성도 사용함
-
메타 속성과 스타일 속성
- 메타 속성
- Markdown 파서가 Markdown 문법의 개별 부분을 식별하기 위해 정의함
- 순수하게 의미 정보를 위한 커스텀 문자열-값 쌍임
- 텍스트의 시각적 모양에는 영향을 주지 않음
- Markdown용 단순화된 AST처럼 볼 수 있음
- 스타일 속성
- 메타 속성으로 표시된 부분 위에 적용되는 시각 속성임
- AppKit과 UIKit이 정의한 내장 문자열-값 쌍임
- 메타 속성
-
속성 갱신을 일으키는 이벤트
- 속성은
NSTextStorage의 Markdown 텍스트와 텍스트에 영향을 주는 설정 변화에 맞춰 동기화됨 - 문서 열기에서는 메타 속성과 스타일 속성을 전체 갱신함
- 텍스트 변경에서는 영향을 받은 부분의 메타 속성과 스타일 속성을 부분 갱신함
- 설정 변경에서는 메타 속성은 바꾸지 않고 스타일 속성만 전체 갱신함
- 속성은
-
갱신 순서와 레이아웃 비용
- 먼저 텍스트 편집 트랜잭션을 시작해 속성 변경을 묶음
- 트랜잭션 없이 속성을 바꾸면 매번
NSLayoutManager의 비싼 레이아웃 재계산이 발생함 - Markdown 문자열을 파싱해 메타 속성으로 표시되는 조각으로 나누며, 설정 변경 시에는 Markdown 구조가 변하지 않으므로 이 단계를 건너뜀
- 글리프 위치나 크기에 영향을 줄 수 있는 시각 속성을 먼저 갱신하고,
NSParagraphStyle은 줄과 문단 레이아웃에 영향을 주는 가장 중요한 속성으로 다뤄짐 - 트랜잭션을 끝낸 뒤에는 레이아웃에 영향을 주지 않는 장식 속성을 갱신함
- 장식 속성은 Apple 용어로 렌더링 속성에 해당하며,
NSTextStorage가 아니라NSLayoutManager에 존재하므로 트랜잭션을 인식하지 않음
-
입력 속성
- 입력 속성(typing attributes) 은 빈 선택 영역에서는 캐럿 앞 위치의 속성, 비어 있지 않은 선택 영역에서는 선택 시작 지점의 속성과 연결됨
- 문자를 입력하면 입력 속성이 새로 삽입된 텍스트에 자동으로 적용됨
- Markdown 편집기에서는 스타일이 Markdown 문법에서 파생되므로 중요도가 낮음
- 리치 텍스트 편집기에서는 스타일을 끄거나 캐럿을 다른 위치로 옮길 때까지 캐럿에 스타일이 붙어 있어 더 중요함
- Paper의 Preview Mode는 Markdown 편집기 안에서도 토글 가능한 입력 속성이 강조되는 리치 텍스트 편집 경험을 제공함
입력 성능과 메타 속성 활용
- 메타, 레이아웃, 장식 속성 분리는 일부 편집기 변경을 빠르게 처리하는 데 유리함
- 라이트·다크 모드 전환은 장식 속성만 갱신하므로 레이아웃을 유발하지 않아 매우 빠름
- 텍스트 크기 조정 같은 설정 변경은 전체 문서 레이아웃 재계산이 필요하지만, Markdown 구조 전체 재파싱까지 하는 것보다는 빠름
-
Markdown 입력 최적화
- 텍스트 편집기의 가장 중요한 성능 요소는 입력 속도임
- Markdown 특성상 텍스트 변경 하나가 문단 전체 스타일에 영향을 줄 수 있음
- 매 키 입력마다 문단 전체를 다시 파싱하고 다시 스타일링하는 방식은 기술적으로 가장 정확하지만, 긴 문단에서는 편집 속도를 늦출 수 있음
- Paper는 입력되는 다음 문자와 주변 문자를 보고 동작을 나누는 알고리듬을 사용함
- 특수 Markdown 기호를 입력하거나 편집 위치 주변에 특수 기호가 있으면 문단 전체를 갱신함
- 그렇지 않으면 입력 속성에 의존해 대부분의 입력 상황에서 편집기 속도를 높임
-
코드 블록 예외와 값 객체 캐시
- 코드 블록은 Paper에서 유일한 다중 문단 Markdown 구조임
- 코드 블록이 있는 문서에서는 키 입력 하나가 전체 문서 스타일에 영향을 줄 수 있음
- Paper는 일정 문자 수를 넘는 문서에서는 코드 블록 처리를 무시해 코드에 관심 없는 다수 사용자에게 빠른 편집기를 유지함
- 이 선택은 개발자 인접 사용자에게도 Paper를 더 유용하게 만듦
- 복잡한 속성 값 객체는 매 키 입력마다 다시 할당되지만, 텍스트 영향 설정이 바뀌지 않는 한 변하지 않으므로 캐시해 재사용함
NSFont/UIFontNSColor/UIColorNSParagraphStyle
-
메타 속성이 쓰이는 기능
- 서식 단축키
- 선택한 Markdown 텍스트의 기존 스타일 정보를 알아야 토글이 가능함
- 선택 영역이 같은 스타일을 완전히 감싸면 해당 스타일을 제거함
- 선택 영역에 같은 스타일이 없으면 스타일을 추가함
- 선택 영역이 같은 스타일을 일부만 감싸면 스타일을 선택 영역으로 옮김
- heading과 blockquote처럼 문단 타입을 정의하는 충돌 스타일은 섞을 수 없어 새 스타일 추가 전에 제거해야 함
- 챕터 이동
- 이전 또는 다음 챕터 경계로 이동할 때 캐럿 위치 기준 heading을 찾는 데 메타 속성을 사용함
- Outline
- 모든 heading을 순회하며, outline 항목을 누르면 해당 챕터로 캐럿이 이동함
- 챕터 재배치
- outline에서 챕터를 재배열할 수 있음
- 형식 변환
- Markdown을 RTF, HTML, DOCX로 변환하려면 텍스트 구조를 알아야 함
- Paper는 외부 라이브러리를 포함하지 않기 때문에 미리 파싱된 텍스트 모델을 순회하며 출력 형식을 만듦
- 서식 단축키
텍스트 컨테이너 수학과 선택 동작
-
줄 길이와 여백 계산
- 텍스트 컨테이너의 핵심 규칙은 선호하는 줄 길이를 유지하고 남은 공간을 양쪽 inset으로 나누는 것임
- heading 태그가 일반 텍스트 흐름 바깥에 배치되는 경우처럼 대칭을 가짜로 맞춰야 하는 상황도 있음
- 이때 텍스트 컨테이너를 왼쪽으로 이동하고, 문단은
NSParagraphStyle로 들여쓰기함 - 충분한 공간이 있으면 여백을 시각적으로 대칭에 가깝게 유지함
- 추가 공간이 없으면 지정한 줄 길이를 유지하는 쪽으로 대칭을 깨지만, 오른쪽 padding이 남아 있을 때까지만 그렇게 함
- padding이 남지 않으면 선호 줄 너비보다 최소 여백이 우선함
min과max조합으로 점진적으로 접히는 여백을 구현할 수 있음
-
선택 앵커
- 텍스트 선택에는 항상 앵커 지점이 있음
- Mac에서는 클릭 후 드래그할 때 클릭한 지점이 기준이 되며, 그 지점을 지나면 선택 증감 방향이 반대로 바뀜
- iOS에서는 한쪽 선택 핸들을 드래그하면 반대쪽이 앵커가 되고, 반대의 경우도 동일함
- 키보드로 선택을 확장할 때도 같은 논리가 적용됨
Option+왼쪽·오른쪽 화살표로 단어 경계를 이동하고,Shift까지 함께 누르면 단어 단위로 선택함- 클릭·드래그로 선택한 뒤 키보드로 계속 확장하거나 줄여도 최초 클릭 지점이 앵커로 남음
-
선택 affinity
- 선택 affinity는 줄바꿈 경계에서 삽입 지점이 이전 줄 마지막 문자 뒤에 나타날지, 다음 줄 첫 문자 앞에 나타날지를 결정함
- 화살표 키로 캐럿을 움직이면 줄바꿈 지점의 공백 주변에서 단순히 줄을 전환함
- 단축키로 줄 끝으로 이동하면 캐럿이 같은 줄에 머물며 줄바꿈 공백의 오른쪽에 붙을 수 있음
TextView는 이와 비슷한 선택 affinity 동작을 다른 상황에서도 수행함
-
Undo coalescing
- Undo coalescing은 입력한 텍스트를 언제 하나의 undo 가능한 동작으로 묶을지 결정하는 알고리듬임
TextView는 다른 동작으로 중단되지 않고 입력된 내용을 함께 묶는 단순한 알고리듬을 쓰는 것으로 보임- 중간에 쉬었다가 계속 입력해도 하나의 undo로 묶이고, 줄바꿈도 coalescing을 끊지 않음
- Sublime은 입력 중 별도 단어를 undo 단위로 묶는 방식을 선호하며, 긴 단어를 입력할 때는 더 작은 undo 단위로 나누기도 함
- 이 동작에는 정답이 없으며 편집기마다 서로 다른 전략을 가질 수 있음
UTI, pasteboard, 앱 간 데이터 교환
-
Uniform Type Identifiers
- UTI는 데이터 타입이 부모 데이터 타입을 상속하듯
conform to하는 계층형 시스템임 public.*타입은 Apple이 정의하며public.html,public.jpeg같은 널리 쓰이는 형식을 식별함- 개발자는 충돌을 피하기 위해 역도메인 이름 체계로 자체 식별자를 만들 수 있음
- 계층형 시스템의 장점은 앱이 모든 텍스트 형식을 볼 수 있다면 개별 형식을 모두 나열하지 않고
public.text만 선언할 수 있다는 점임 - Paper는 모든 텍스트 파일을 열 수 있다고 선언하며, highlighting은 없더라도
.html,.rtf같은 텍스트 형식을 열 수 있음 - 파일은 크로스플랫폼 개념이므로 사실상 파일 확장자가 식별자로 쓰임
- 호환성을 위해 모든 UTI는 연결된 하나 이상의 파일 확장자를 정의할 수 있음
- Markdown은 널리 쓰이지만 Apple이 정의하지 않은 형식이라 세미공개 UTI와 관련된 까다로운 사례가 생김
- UTI는 데이터 타입이 부모 데이터 타입을 상속하듯
-
pasteboard와 여러 표현
- Apple의 pasteboard는 UTI를 직렬화된 텍스트 또는 바이너리 데이터에 매핑하는 딕셔너리임
- Xcode Additional Tools의 Clipboard Viewer로 pasteboard 내용을 실시간으로 확인할 수 있음
- 하나의 복사 동작은 같은 데이터의 여러 표현을 동시에 씀
- 일부 앱은 하위 호환성을 위해
NeXT Rich Text Format v1.0 pasteboard type같은 레거시 비-UTI 식별자도 씀 - 에디터는 일반적으로 자신이 처리할 수 있는 가장 풍부한 형식을 선택함
- 리치 텍스트 편집기의
Paste and Match Style또는Paste as Plain Text는 pasteboard의 일반 텍스트 형식을 사용하게 함 - 붙여넣은 텍스트에 적용되는 스타일은 보통 입력 속성에서 가져옴
- 드래그 앤 드롭도 다른 pasteboard를 기반으로 동작함
- 표준 copy-paste에는 general pasteboard가 사용되고, 맞춤형 앱 간 상호작용에는 커스텀 pasteboard를 만들 수 있음
-
RTF와
NSAttributedString- RTF는 기본적으로
NSAttributedString의 직렬화 형태이며, 반대로NSAttributedString은 RTF의 프로그래밍 인터페이스임 TextView는NSAttributedString의 자식 클래스인NSTextStorage위에서 동작하므로 pasteboard와 기본적으로 호환됨- 별도 코딩 없이 내용을 pasteboard에 복사할 수 있음
- RTF는 기본적으로
-
Markdown UTI의 앱 간 문제
- Markdown 에디터 간 복사·붙여넣기에서는 두 앱이 표준 프로토콜을 구현해도 문제가 생길 수 있음
- 첫 번째 에디터가 Markdown을
public.text, 리치 텍스트 표현을public.rtf로 내보내면 두 번째 에디터는 네이티브 Markdown임을 알 수 없어public.rtf를 선택할 수 있음 - 이 경우 불필요한 이중 변환이 일어나고, 앱마다 Markdown↔RTF 변환 방식이 달라 추가 줄바꿈 같은 작은 서식 문제가 생길 수 있음
- Markdown과 RTF의 근본적인 스타일 차이도 드러날 수 있음
- 두 앱이 모두
net.daringfireball.markdownUTI를 내보내고public.rtf보다 우선해야 자연스럽게 동작함 - Paper는 Markdown UTI를 내보내려 했지만, 다른 앱들이 리치 텍스트보다 이를 우선하지 않는 것으로 보였음
- Pages는
net.daringfireball.markdown을public.rtf보다 우선하지만, 리치 텍스트로 변환하지 않고 원본 Markdown 문자열을 그대로 삽입하는 동작을 보임 - 이 때문에 Paper는 Markdown UTI를 제거함
-
Paper가 RTF를 제공하는 이유와 iOS 공유
- Paper는 리치 텍스트 편집기로 자연스럽게 복사·붙여넣기되도록 RTF를 제공함
- 좋은 OS 시민으로서 복사된 데이터를 나타내는 여러 형식을 제공해야 수신 앱이 처리 가능한 가장 풍부한 형식을 고를 수 있음
- Paper에서 Markdown 텍스트를 복사해 Mail 앱에 붙여넣으면 Markdown 변형이 아니라 보기 좋게 서식화된 리치 텍스트로 붙여넣어짐
- iOS의 공유 기능은 copy-paste와 비슷하지만 UI가 더해진 형태임
- iOS 공유에서는 앱이 여러 형식으로 데이터를 내보내고 수신 앱이 원하는 형식을 선택함
- pasteboard와 달리 iOS 공유의 상단 앱 목록에 나타나려면 앱이 커스텀 UI를 가진 share extension을 명시적으로 제공해야 함