이 글은 Today I Learn 시리즈의 132번째 기록입니다. (총 133개)

외부 그림을 아이콘·도안으로 들이기

여백을 안 자르면 그림이 작아진다

투명 배경 PNG를 정해진 칸에 그리면, 그림 주위의 빈 여백까지 자리를 차지한다. 같은 크기로 그려도 실제 그림은 작게 보인다.

받아온 게임 에셋 묶음에서 이 처리가 빠졌더니 그림이 화면을 채우는 비율이 0.19였다. 자르고 정사각으로 다시 채우니 0.46이 됐다.

im = im.crop(im.getchannel('A').getbbox())   # 알파 기준 실제 내용 범위
w, h = im.size
s = max(w, h)
sq = Image.new('RGBA', (s, s), (0, 0, 0, 0))  # 정사각 — 비율이 안 찌그러지게
sq.paste(im, ((s - w) // 2, (s - h) // 2))

작아지는 것만 문제가 아니다. 그림이 작으면 색을 줄이는 단계에서 각 색이 차지하는 면적도 같이 줄어, “면적이 너무 작은 색”으로 걸려 탈락한다. 여백을 자르자 통과 도안이 69개에서 121개로 늘었다.


흰 배경에 합성된 그림은 가장자리에 흰 테가 남는다

투명 PNG인데도 어두운 면 위에 올리면 흰 윤곽이 보이는 경우가 있다. 원래 흰 배경에 그려진 그림에서 알파만 뽑아내면, 반투명한 가장자리 픽셀의 색이 흰색과 섞인 채로 남기 때문이다.

한 아이콘은 반투명 픽셀의 47%가 흰색에 가까웠다.

고치는 방법은 알파는 그대로 두고 색만 안쪽에서 바깥으로 번지게 덮는 것이다.

core = (alpha > 0.98)[..., None]           # 확실히 불투명한 안쪽
bleed = Image.fromarray(np.where(core, rgb, 0).astype('uint8'))
for _ in range(6):
    bleed = bleed.filter(ImageFilter.MaxFilter(5))   # 안쪽 색을 밖으로 확장
out = np.where(core, rgb, np.array(bleed))

투명한 곳의 색은 눈에 안 보이지만, 반투명 구간에서 배경과 섞일 때 드러난다. 그래서 “안 보이는 색”도 채워 둬야 한다.


표시 크기 × 화면 배율만큼만 있으면 된다

1280픽셀 이미지를 18픽셀 아이콘으로 쓰면 나머지는 전부 낭비다. 가장 크게 그리는 자리를 재고 화면 배율(고밀도 화면은 3배)을 곱한 만큼만 남긴다.

쓰임 원본 줄인 뒤
하트 아이콘 (최대 48) 1280px · 1547KB 256px · 54KB
코인 아이콘 (최대 22) 1032px · 1774KB 256px · 96KB
카드 (최대 110) 1125px · 1755KB 384px · 171KB

줄인 이미지를 더 작게 그릴 때는 밉맵을 쓰는 품질 설정이 필요하다. 안 그러면 가장자리가 거칠어진다.

Image.asset(asset, width: size, height: size,
            filterQuality: FilterQuality.medium)

픽셀로 그린 그림은 최근접으로 줄인다

부드럽게 줄이는 방식은 이웃한 픽셀을 섞는다. 사진에서는 자연스럽지만 픽셀 그림에서는 경계마다 원래 없던 중간색이 생기고, 그 색이 색 줄이기 단계에서 원래 색을 밀어낸다.

직접 그린 도안을 100픽셀에서 64픽셀로 줄였더니 파란 리본과 금색 동전이 결과에서 통째로 사라졌다.

# ❌ 픽셀 그림에서는 색이 사라진다
im.resize((64, 64), Image.LANCZOS)
# ✅ 원래 색만 남는다
im.resize((64, 64), Image.NEAREST)
축소 방식 결과 색
LANCZOS 배경 · 빨강 · 회색 · 자주
NEAREST 배경 · 빨강 · 파랑 · 금색 · (얇은 경계색 2개)

얇은 경계색은 지운다

손으로 그린 그림에는 가장자리에 부드럽게 처리된 픽셀이 남는다. 색을 줄이면 그게 면적 1~2%짜리 색으로 잡힌다.

이 게임에서는 색 하나가 바구니 하나다. 칸 몇 개를 위해 바구니 하나를 쓰게 되므로 지우는 게 맞다.

count = collections.Counter(v for row in grid for v in row)
total = sum(n for i, n in count.items() if i)      # 배경 제외
keep = [0] + [i for i in range(1, len(palette))
              if count[i] / total >= 0.03]         # 3% 미만은 버린다

버릴 때는 가장 가까운 남은 색으로 흡수시키고 인덱스를 다시 매긴다. 배경은 흡수 대상이 아니다 — 없어지면 그림이 잘린다.


색을 코드로 다루기

밝기를 곱하면 밝은 부분이 날아간다

어두운 아이콘을 밝히려고 색에 배수를 곱했다. 평균 밝기는 원하는 값이 됐는데 그림이 이상해졌다.

이미 밝은 부분은 곱하면 최댓값을 넘어 잘린다. 잘린 픽셀은 전부 순백이 되어 그 영역의 무늬가 사라진다.

방법 평균 밝기 채도 순백으로 날아간 면적
원본 151 0.59 0.5%
색에 1.45 곱하기 203 0.53 58.8%
세 채널에 감마 174 0.59 0.5%
밝기에만 감마 178 0.59 0.7%

곱하기는 밝기를 60% 영역과 맞바꾼 셈이었다.


감마를 세 채널에 걸면 색이 바랜다

감마는 어두운 쪽을 끌어올리면서 최댓값을 안 넘기니 잘림이 없다. 그런데 금색이 베이지가 됐다.

세 채널에 각각 걸면 원래 값이 낮은 채널이 비율상 더 많이 올라간다. 금색은 파랑이 낮으니 파랑이 상대적으로 크게 올라 회색에 가까워진다. 이게 채도가 떨어지는 이유다.

해결은 밝기(채널 최댓값)에만 감마를 걸고 나머지를 비례로 따라가게 하는 것이다. 색상과 채도의 비율이 유지된다.

mx = np.max(rgb, axis=2)                       # 밝기 = 채널 최댓값
scale = np.power(mx, gamma) / mx                # 밝기만 감마
out = rgb * scale[..., None]                    # 나머지는 비례로 따라간다

색 필터는 알파가 곱해진 색에 걸린다

밝히는 걸 이미지에 굽지 않고 그릴 때 하고 싶어서 색 행렬 필터를 봤다. 밝기를 올리려면 상수를 더하는 게 자연스러운데, 그러면 안 된다.

이 필터는 알파가 이미 곱해진 색에 적용된다. 완전히 투명한 픽셀은 색이 0인데, 여기에 상수를 더하면 색이 생겨 투명해야 할 바깥까지 사각형으로 드러난다.

// ✅ 곱셈만 — 0에 무엇을 곱해도 0이라 투명한 곳은 그대로다
ColorFilter.matrix([
  k, 0, 0, 0, 0,
  0, k, 0, 0, 0,
  0, 0, k, 0, 0,
  0, 0, 0, 1, 0,
])

그래서 밝히는 건 이미지에 굽고, 낮추는 것만 그릴 때 곱셈으로 했다. 낮추는 쪽은 날아갈 밝은 부분이 없어 안전하다.


채도를 올릴 수 있는 한계는 그림마다 다르다

색이 흐린 도안을 일괄로 진하게 하면, 이미 진했던 색은 과해지고 서로 가까웠던 색은 붙어 버린다. 이 게임에서 색이 붙으면 어느 바구니에 부어야 할지 알 수 없게 된다.

그림마다 “색 구분 기준을 안 깨는 최대 배율”을 따로 찾고, 절대 상한도 뒀다.

for k in (2.0, 1.8, 1.6, 1.4, 1.25, 1.15, 1.05):
    if separation_ok(boosted(palette, k)):
        break        # 통과하는 가장 큰 배율에서 멈춘다

225개 중 210개가 개선됐고 15개는 그대로 뒀다. 백자·백조처럼 흰색이 본질인 도안은 올리면 그 도안이 아니게 된다.


화면 연출

회전만으로는 뒤집혀 보이지 않는다

카드를 Y축으로 180도 돌렸는데, 뒤집히는 게 아니라 가로로 납작해졌다 돌아오기만 했다.

원근이 없으면 3D 회전이 폭 변화로만 투영된다. 행렬에 깊이 항을 넣어야 가까운 쪽이 커지고 먼 쪽이 작아지면서 도는 것처럼 보인다.

Matrix4.identity()
  ..setEntry(3, 2, 0.0015)   // 원근 — 없으면 납작해지기만 한다
  ..rotateY(turn * pi)

값이 클수록 왜곡이 심하다. 0.001~0.002 정도가 자연스럽다.


반 넘게 돌면 거울상이 된다

90도를 넘어가면 뒷면을 보는 상태라 내용이 좌우로 뒤집힌다. 앞면일 때는 상관없지만 뒷면에 다른 그림을 보여주려면 되돌려야 한다.

faceUp
  ? Transform(                                   // 180도 한 번 더
      transform: Matrix4.rotationY(pi),
      child: backSide,
    )
  : frontSide

안 하면 액자 장식이 거울에 비친 것처럼 나온다. 대칭이 아닌 그림에서만 눈에 띈다.


그림 그리는 객체는 프레임마다 새로 만들어진다

터지는 조각이 퍼져 나가지 않고 매 프레임 다른 자리에 나타났다. 조각의 각도와 속도를 그리는 객체 안에서 정하고 있었던 게 원인이다.

애니메이션이 도는 동안 그 객체는 계속 새로 만들어진다. 안에서 무작위 값을 뽑으면 프레임마다 다른 값이 나온다.

// ❌ 프레임마다 다른 자리로 튄다
class _FleckPainter extends CustomPainter {
  final flecks = List.generate(46, (_) => _Fleck.random());
}

// ✅ 한 번 만들어 화면 상태로 들고 있는다
setState(() => _flecks = _Fleck.burst(Random()));

시간에 따라 변해야 하는 건 진행도 하나뿐이고, 나머지는 시작할 때 정해져야 한다.


되감기 대신 위상을 밀면 따로 움직인다

카드 아홉 장이 숨쉬듯 움직이게 했더니 전부 같은 박자로 뛰어 기계처럼 보였다.

되감기(reverse) 방식은 모두 같은 곡선을 그린다. 한 방향으로 도는 값을 sin으로 읽으면 카드마다 시작 위치를 밀 수 있다.

scale: 1 + 0.04 * sin(2 * pi * (idle.value + phase))   // phase = i / 9

컨트롤러는 하나만 쓰고 위상만 다르게 준다. 물결처럼 번갈아 움직인다.


도안과 난이도

출처는 이름이 아니라 실제 소스로 판정한다

여러 곳에서 받은 도안을 구분하려고 이름 앞머리로 갈랐다. 접두어가 붙은 것들은 맞았는데, 접두어가 없으면 전부 “직접 만든 것”으로 처리했다.

표준 이모지에도 apple·heart 같은 평범한 이름이 많았다. 199개가 직접 만든 것으로 잡혔는데 실제로는 17개였다.

# ❌ 이름은 사람이 붙인 라벨이지 출처가 아니다
def source(name):
    return "custom" if not name.startswith(("k_", "f_")) else ...

# ✅ 실제 소스 목록을 읽어 판정
from quantize import EMOJI_NAMES
if name in set(EMOJI_NAMES.values()):
    return "emoji"

난이도는 색 개수가 아니라 면적 분포가 정한다

같은 3색 도안 둘을 여러 난이도 자리에 넣어 봤더니 결과가 딴판이었다.

도안 색 면적 가장 큰 색 목표 난이도 적중
태극 45 / 41 / 14 45% 6칸 중 5칸
복주머니 86 / 8 / 6 86% 6칸 중 0칸

한 색이 그림의 대부분을 차지하면 부을 순서를 고민할 거리가 없다. 벽을 아무리 깔아도 어려워지지 않는다. 반대로 두 색이 비슷한 덩어리면 어느 쪽을 먼저 붓느냐가 판을 좌우해서, 벽만으로 쉬운 자리부터 어려운 자리까지 끌고 다닐 수 있다.

새로 그린 게 문제인 줄 알았는데 예전 것도 같은 분포라 같은 결과였다. 그림 솜씨가 아니라 구성의 문제다.


정렬로 배정하면 하나만 바꿔도 전부 밀린다

도안 두 개를 교체하고 레벨을 다시 만들었더니 500개 중 469개의 그림이 바뀌었다.

난이도 점수로 정렬해 앞에서부터 배정하는 구조라, 한 도안의 점수가 달라지면 정렬 위치가 밀리고 뒤따르는 배정이 전부 어긋난다. 바꾼 것과 무관한 레벨까지 영향을 받는다.

이미 나간 콘텐츠에서는 치명적이다. 300단계까지 진행한 사람이 지나온 단계가 전부 다른 그림이 된다.

  • 아직 사용자가 없으면 전체 재생성이 제일 간단하다
  • 내보낸 뒤에는 바뀐 것을 쓰는 레벨만 다시 만들어야 한다. 그러려면 자리가 고정이므로 새 그림의 난이도가 옛 그림과 비슷해야 한다

릴리스 서명

설정을 비워 두는 것으로는 사고를 못 막는다

서명 키가 없으면 빌드가 실패하도록 하려고 서명 설정을 비웠다. 확인하려고 키를 치우고 돌려 보니 빌드가 그냥 성공했다.

서명 없는 결과물이 나오는데 파일 이름도 평소와 같았다. 스토어가 거부할 파일을 만들어 놓고 성공한 줄 알게 된다 — 디버그 키로 조용히 넘어가던 것과 증상이 똑같다.

// ❌ 빌드가 성공하고 서명 없는 결과물이 나온다
signingConfig = if (keystoreProperties.isEmpty) null else ...

// ✅ 릴리스 작업이 실제 실행 목록에 있을 때만 중단
gradle.taskGraph.whenReady {
    if (keystoreProperties.isEmpty && allTasks.any { it.name.contains("Release") }) {
        throw GradleException("릴리스 서명 키가 없다. key.properties 를 만들어라.")
    }
}

설정 단계에서 무조건 중단하면 키가 없는 환경에서 개발 빌드까지 막힌다. 그래서 실행 목록을 보고 판단한다.

“이렇게 하면 막힐 것”이라고 적어 두는 것과 실제로 막히는지 확인하는 건 다른 일이었다. 안전장치는 일부러 부러뜨려 봐야 한다.


업로드 키는 앱 서명 키가 아니다

새로 올리는 앱은 스토어가 서명 키를 대신 보관한다. 우리가 만드는 키는 “업로드 키”로, 우리가 올린 파일이 맞는지 확인하는 용도다.

  • 업로드 키를 잃으면 재설정을 요청할 수 있다. 며칠 걸리지만 복구된다
  • 예전처럼 “키를 잃으면 앱을 영영 못 고친다”가 아니다

그래도 백업은 해야 한다. 재설정하는 동안 업데이트가 막힌다.


키 파일은 저장소 밖에 둔다

저장소 안에 두고 제외 목록에 넣는 방식은 한 가지를 못 막는다. 추적되지 않는 파일을 정리하는 명령 한 번, 또는 저장소를 다시 받는 순간 사라진다.

  • 키 파일은 저장소 밖 별도 폴더에, 설정 파일이 절대경로로 가리키게
  • 백업은 서로 다른 매체 두 곳 이상에. 비밀번호 관리자에 파일을 첨부하면 비밀번호와 한자리에 모인다
  • 적어 두는 메타데이터(별칭·지문·만료)만으로는 같은 키를 다시 못 만든다. 파일 자체가 백업돼야 한다

비밀번호는 저장소에 적지 않는다. 비공개 저장소라도 이력은 남고, 공개로 바꾸거나 사람을 추가하는 순간 드러난다.


언어 전환

번역이 빠진 걸 컴파일이 잡게 만든다

문구를 담는 클래스를 만들 때 두 가지 방법이 있다.

// ❌ 기본 구현을 두고 덮어쓰는 방식 — 빠뜨리면 조용히 원문이 나온다
class Strings {
  String get shop => '상점';
}
class StringsEn extends Strings {
  @override String get shop => 'Shop';
  // heartsFull을 안 적었다 → 영어 화면에 한국어가 섞여 나온다
}

// ✅ 추상 멤버 — 한쪽에 빠지면 컴파일이 막힌다
abstract class Strings {
  String get shop;
  String get heartsFull;
}

문구가 100개쯤 되면 하나쯤 빠뜨리는 건 시간 문제다. 실행해 보고 눈으로 찾는 대신 빌드가 막히게 두는 편이 낫다.


언어는 화면마다 다를 일이 없으니 전역으로 둔다

보통은 InheritedWidget으로 내려보내지만, 이 경우엔 전역 변수 하나가 낫다고 봤다.

  • 언어는 앱 전체가 한 번에 바뀌는 값이라 화면마다 다를 일이 없다
  • BuildContext가 없는 곳(그림 그리는 객체, 순수 함수)에서도 문구를 읽어야 한다
  • 화면마다 구독 코드를 넣지 않아도 된다

대신 바뀌었을 때 다시 그리는 걸 직접 챙겨야 한다.


화면 하나만 다시 그리면 뒤에 남은 화면이 안 바뀐다

설정에서 언어를 바꾸고 그 화면만 갱신했더니, 뒤로 나갔을 때 목록 화면이 옛 언어 그대로였다. 이미 만들어진 위젯은 다시 만들 이유가 없으니 당연한 결과다.

루트에서 키를 바꿔 트리를 통째로 새로 만들게 했다.

home: MainShell(
  key: ValueKey(meta.intPref('lang')),  // 키가 바뀌면 전부 새로 만든다
  ...
)

언어에 따라 변하는 값은 상수가 될 수 없다

문구를 상수로 두고 있었다면 언어 전환을 붙이는 순간 전부 컴파일 오류가 난다. 상수는 빌드 시점에 값이 정해져야 하는데, 문구는 실행 중에 바뀐다.

// ❌ 상수는 실행 중에 바뀌는 값을 담을 수 없다
const kAdNotReadyMsg = t.adNotReady;
items: const [ BottomNavigationBarItem(label: t.tabShop) ]

// ✅ 매번 읽는 getter로, 리스트에서는 const를 벗기고 안쪽 위젯에만 남긴다
String get kAdNotReadyMsg => t.adNotReady;
items: [ BottomNavigationBarItem(icon: const Icon(...), label: t.tabShop) ]

키가 이름표인 맵도 마찬가지다. static const Map 이었던 아이템 이름표를 getter로 바꿔야 했다.


테스트는 기기 언어를 따라간다

문구를 옮기고 나서 위젯 테스트 12개가 한꺼번에 깨졌다. 테스트가 찾는 문구는 한국어인데 테스트 환경의 언어가 영어라, 기본값(기기 설정 따르기)이 영어를 골랐다.

setUpAll(() => applyLang(AppLang.ko));   // 문구 원문 언어로 고정

영어 번역이 빠지는 사고는 앞의 추상 클래스가 컴파일에서 잡으므로, 테스트까지 두 언어로 돌릴 필요는 없다고 봤다.


레이아웃은 만들 때 쓴 언어에 맞춰져 있다

한국어로 만든 화면에 영어를 넣으면 넘친다. 설정의 토글 줄이 37픽셀 넘쳤는데, 한국어로 “출시 전 제거”였던 자리가 영어로는 “Remove before release”였다.

// ❌ 고정폭 텍스트 둘 + 스위치 — 길어지면 밀어낸다
Row(children: [Text(label), Text(note), Spacer(), Switch(...)])

// ✅ 남는 폭 안에서 줄어들고, 그래도 길면 말줄임
Row(children: [
  Expanded(child: Row(children: [
    Flexible(child: Text(label, overflow: TextOverflow.ellipsis)),
    Flexible(child: Text(note, overflow: TextOverflow.ellipsis)),
  ])),
  Switch(...),
])

한 번 고치고 끝날 문제가 아니라서, 영어로 화면을 끝까지 그려 보고 넘치면 실패하는 테스트를 넣었다. 넘침은 테스트에서 예외로 잡히므로 따로 검사식을 쓰지 않아도 된다.


글자만 담은 서체는 문구를 고칠 때마다 다시 구워야 한다

한글 서체 전체는 웨이트당 2MB가 넘어서, 실제 쓰는 글자만 남긴 파일을 쓰고 있다. 문구를 추가하면 그 글자가 서체에 없어서 네모(□)로 나온다.

문구를 파일 하나로 모으는 작업도 결국 글자 구성이 바뀌는 일이라 다시 구웠다(394자, 웨이트당 184KB).


받을 수 없는 양을 적어 두면 속인 게 된다

광고를 보면 하트 5개를 주는데, 최대치가 5라서 4개 있을 때 보면 1개만 들어오고 4개는 버려진다. 하루 한 번뿐인 기회를 5분의 1 값에 쓰는 셈이다.

  • 해결: 카드에 정량(+5)이 아니라 지금 실제로 들어올 개수를 표시했다
  • 막지는 않았다. 지금 당장 한 판 하려고 1개를 받는 건 손해가 아니라 선택이다. 숫자가 사실대로 적혀 있으면 그건 속임수가 아니다

한도가 있는 보상은 “주는 양”이 아니라 “받는 양”을 보여줘야 한다.


Series: Today I Learn

1 C++ 자료형(Data Type) 2 MD5 vs pHash 3 C++에서 함수의 선언과 정의 4 Tkinter padx, pady 5 메모리와 포인터 변수 6 Call by Value, Call by Reference, Call by Pointer 비교 7 const 8 Gemfile — Jekyll 프로젝트의 의존성 파일 9 kramdown-parser-gfm — Jekyll의 GFM 파서 10 파서(Parser) 11 AHU vs OHU 12 I might try it vs I'll try it 뉘앙스 차이 13 SESSION_EXPIRE_AT_BROWSER_CLOSE=True 14 configuration key 15 Git stash vs discard 16 subprocess.Popen으로 Windows 탐색기에 명령어를 전달 17 Post 잔디 분석하기 18 Google Sheets Sync 최적화 19 DSL (Domain Specific Language)과 GPL (General Purpose Language) 20 마크다운 표 그리는 방법 21 쿼리 파라미터(Query Parameter). 기존 QR코드 재활용 22 Django 보안 취약점 점검 및 수정 23 OOP Object-Oriented Programming 객체 지향 프로그래밍 24 Fernet 대칭 암호화 25 Jekyll 코드블록 안의 Liquid 태그 26 습관만들기 Pawbit TIL 1 27 insertOnConflictUpdate vs DoUpdate(target) 28 세션 필터 29 아코디언(Accordiaon) UI를 펼친상태로 만들기 30 input의 step 31 습관만들기 Pawbit TIL 2 32 습관만들기 Pawbit TIL 3 33 Word Cloud 34 Google Sheets를 데이터 버스로(with AppSheet) 35 Django 모델 텍스트 필드 자동 수집 패턴 36 localStorage로 섹션 토글 상태 유지 37 순차 ID 생성(`select_for_update()` + `max()` 조합) 38 역참조 검색과 distinct() 39 xlsx 다운로드와 로딩 오버레이 충돌 40 Android 파일 공유 MIME 타입 41 AssetManifest — Flutter 빌드 타임 asset 목록 런타임 조회 42 크레센도 Phaser + TypeScript TIL 1 43 크레센도 Phaser + TypeScript TIL 2 44 크레센도 Phaser + TypeScript TIL 3 45 크레센도 Phaser + TypeScript TIL 4 46 크레센도 Phaser + TypeScript TIL 5 47 여둘까 Office Layout TIL 1 48 습관만들기 Pawbit TIL 4 49 여둘까 Office Layout TIL 2 50 여둘까 Office Layout TIL 3 51 크레센도 Phaser + TypeScript TIL 6 52 UTF-8 BOM과 PowerShell 파일 쓰기 53 소리꽃 KeyBloom TIL 1 54 소리꽃 KeyBloom TIL 2 55 소리꽃 KeyBloom TIL 3 56 소리꽃 KeyBloom TIL 4 57 소리꽃 KeyBloom TIL 5 58 습관만들기 Pawbit TIL 5 59 여둘까 Office Layout TIL 4 60 소리꽃 KeyBloom TIL 6 61 소리꽃 KeyBloom TIL 7 62 소리꽃 KeyBloom TIL 8 63 메트로놈 Simple Metronome TIL 1 64 소리꽃 KeyBloom TIL 9 65 메트로놈 Simple Metronome TIL 2 66 소리꽃 KeyBloom TIL 10 67 습관만들기 Pawbit TIL 6 68 크레센도 Phaser + TypeScript TIL 7 69 메트로놈 Simple Metronome TIL 3 70 습관만들기 Pawbit TIL 7 71 정적 블로그 SEO 정비와 Pagefind 검색 도입 72 크레센도 Phaser + TypeScript TIL 8 73 메트로놈 Simple Metronome TIL 4 74 습관만들기 Pawbit TIL 8 75 안드로이드 AudioTrack 연속 재생, 실측 피커 정렬, 카메라 토치 플래시 76 메트로놈 Simple Metronome TIL 5 77 모래게임 Sandrop TIL 1 78 습관만들기 Pawbit TIL 9 79 메트로놈 Simple Metronome TIL 6 80 모래게임 Sandrop TIL 2 81 모래게임 Sandrop TIL 3 82 습관만들기 Pawbit TIL 10 83 습관만들기 Pawbit TIL 11 84 메트로놈 Simple Metronome TIL 7 85 모래게임 Sandrop TIL 4 86 모래게임 Sandrop TIL 5 87 습관만들기 Pawbit TIL 12 88 모래게임 Sandrop TIL 6 89 소리꽃 KeyBloom TIL 11 90 모래게임 Sandrop TIL 7 91 모래게임 Sandrop TIL 8 92 소리꽃 KeyBloom TIL 12 93 모래게임 Sandrop TIL 9 94 소리꽃 KeyBloom TIL 13 95 모래게임 Sandrop TIL 10 96 소리꽃 KeyBloom TIL 14 97 소리꽃 KeyBloom TIL 15 98 소리꽃 KeyBloom TIL 16 99 콘티온 Conti On TIL 1 100 소리꽃 KeyBloom TIL 17 101 콘티온 Conti On TIL 2 102 습관만들기 Pawbit TIL 13 103 콘티온 Conti On TIL 3 104 모래게임 Sandrop TIL 11 105 소리꽃 KeyBloom TIL 18 106 여둘까 Office Layout TIL 5 107 모래게임 Sandrop TIL 12 108 모래게임 Sandrop TIL 13 109 콘티온 Conti On TIL 4 110 모래게임 Sandrop TIL 14 111 소리꽃 KeyBloom TIL 19 112 소리꽃 KeyBloom TIL 20 113 소리꽃 KeyBloom TIL 21 114 소리꽃 KeyBloom TIL 22 115 습관만들기 Pawbit TIL 14 116 모래게임 Sandrop TIL 15 117 모래게임 Sandrop TIL 16 118 소리꽃 KeyBloom TIL 23 119 소리꽃 KeyBloom TIL 24 120 소리꽃 KeyBloom TIL 25 121 소리꽃 KeyBloom TIL 26 122 온실 GreenHouse TIL 1 123 콘티온 Conti On TIL 5 124 모래게임 Sandrop TIL 17 125 소리꽃 KeyBloom TIL 27 126 온실 GreenHouse TIL 2 127 콘티온 Conti On TIL 6 128 모래게임 Sandrop TIL 18 129 콘티온 Conti On TIL 7 130 모래게임 Sandrop TIL 19 131 소리꽃 KeyBloom TIL 28 132 모래게임 Sandrop TIL 20 읽는 중 133 모래게임 Sandrop TIL 21