ZEROEQUALTWO
ZEROEQUALTWO

안녕하세요! 이곳은 제가 학습한 것들과 일상의 순간들을 모아놓은 공간입니다. 이곳에서 유익하고 즐거운 시간을 보내시길 바랍니다.

Review
Study NotesLife Hack
© 2025 — Zeroequaltwo. All Rights Reserved.
공부
cover_image
On August 4, 2026머리말 회사에서 AI 학습 데이터 버전 관리에 대해 찾아보다가 DVC에 대해 공부하게 됐다. 안 까먹을라고 게시글로 정리해둔다. 공부한 내용 DVC란? DVC는 재현 가능한 머신러닝 프로젝트를 만들 수 있도록 도와주는 커맨드라인 툴이자 VS Code 확장 프로그램 DVC가 데이터를 관리하는 방식 원천 소스가 originalsource 폴더에 있다고 치자. 여기서 dvc add originalsource/를 치면 두 가지가 일어난다. original_source/ 안의 파일들이 캐시(.dvc/cache/)로 복사됨 original_source.dvc라는 포인터 파일이 생김 캐시 안에서는 파일이 MD5 해시값으로 저장된다. 파일시스템 dvc에서 원천 소스와 cache 사이의 관계를 어떻게 설정하는지 알려면 아래 개념을 알아야 한다. inode inode는 모든 파일시스템 객체(일반 파일, 디렉토리, 심볼릭 링크, 디바이스 파일 등)가 공통으로 갖는 "메타데이터 구조"이다. 여기서는 각 파일이 갖는 고유한 "주민등록증" 개념으로 이해해도 된다. 파일 이름을 변경하면 블록이 새로 생기나? 파일 이름은 inode 소속이 아니고, 디렉토리가 들고 있는 정보다. 그래서 mv로 이름을 바꿔도 데이터 블록은 그대로다. 블록 디스크를 4KB 정도로 잘라놓은 조각으로, 실제 데이터가 담기는 최소 단위이다. 워킹 디렉토리와 cache 사이의 관계 위에서 워킹 디렉토리(original_source/) 안의 파일들이 캐시(.dvc/cache/)로 복사된다고 언급했다. 이때 두 개를 연결하는 방식은 hardlink, symlink, reflink, copy 네 가지가 있다. Hardlink 같은 inode를 가리키는 경로를 하나 더 만든다. 한쪽 수정하면 양쪽 다 바뀜 → 같은 inode니까 당연함 한쪽 지워도 나머지 경로로 데이터 접근 가능 같은 파일시스템 안에서만 가능 → inode가 하나의 파일 시스템에서만 유일하기 때문임 디렉토리엔 못 씀 → 디렉토리 자체를 하드링크 걸어버리면 트리 구조에 순환 참조가 생길 수 있음(= 내 안의 나를 바라보는 내 안의 나를 바라보는 내 안의 나를 바라보는...) → 대부분의 유닉스 계열 파일시스템은 아예 OS 레벨에서 디렉토리 하드링크 생성을 막아놓음 Symlink "이 경로면 저기로 가라"는 리다이렉트 메모이다. 원본 지우면 symlink는 끊김 (dangling link) 다른 파일시스템, 다른 서버 경로도 가리킬 수 있음 → hardlink나 reflink처럼 inode 값을 저장하지 않고, 경로 문자열만 저장하기 때문에 찾아갈 수만 있으면 어디에 있든 상관없음 ls -la 치면 -> 화살표로 보임 Reflink 같은 데이터 블록을 두 inode가 독립적으로 참조한다. 한쪽 수정하면 그 순간 그 파일만 새 블록으로 갈라짐 → hardlink는 같은 inode를 공유하기 때문에 한 쪽을 고치면 다른 쪽도 수정되지만, reflink는 완전히 독립적임 두 inode는 대등한 관계로, 어느 쪽도 원본이 아님 APFS(macOS), btrfs, XFS에서만 지원하고, ext4는 지원 안함 Copy 말 그대로 데이터 용량 두 배란 뜻이다. 비교표 | | hardlink | symlink | reflink | |---|---|---|---| | 원본 삭제 시 | 데이터 유지 | 깨짐 | 데이터 유지 | | 수정 시 | 양쪽 반영 | 양쪽 반영 | 각자 독립 | | 다른 파일시스템 | 불가 | 가능 | 불가 | | 디렉토리 | 불가 | 가능 | 가능 | | 관계 | 대등 | 종속 | 대등 | DVC는 실제로 어떤 순서로 링크 방식을 고를까 cache.type의 기본값은 reflink,copy다. 즉 아무 설정도 안 하면 DVC는 reflink를 먼저 시도하고, 안 되면 바로 copy로 넘어간다. hardlink나 symlink는 기본 시도 목록에 아예 없다. 이 두 개를 쓰려면 dvc config cache.type hardlink 처럼 사용자가 직접 지정해야 한다. hardlink나 symlink는 캐시 파일과 워킹 디렉토리 파일이 사실상 같은 데이터를 공유하는 구조라서, 워킹 디렉토리에서 파일을 실수로 고치면 캐시까지 같이 오염될 위험이 있다. (그래서 DVC는 cache.type을 hardlink/symlink로 설정하면 해당 파일들을 자동으로 읽기 전용으로 걸어 보호하고, 수정하려면 dvc unprotect를 쓰라고 안내한다.) reflink는 이런 위험이 없으니까 기본값으로 삼은 거였다. 실제 적용 방식 확인하는 법 캐시 지워도 괜찮은가? 여기서 말하는 '지우다'는 폴더를 그냥 삭제하는 걸 의미하는 건 아니고, dvc가 제공하는 gc를 의미한다. 링크 방식에 따라 다르다. | 방식 | 캐시 삭제 시 original_source/ | |---|---| | reflink | 안전 (블록을 독립적으로 참조) | | hardlink | 안전 (inode가 독립적) | | symlink | 위험 (final이 캐시를 바라보는 구조라 끊김) | | copy | 안전 (완전히 독립적인 사본) | 기본값이 reflink,copy라는 걸 다시 떠올리면, 사실 아무 설정도 안 건드린 보통의 상황에서는 reflink 아니면 copy가 걸려 있을 확률이 높다는 뜻이고, 그러면 캐시를 지워도 대부분 안전하다는 얘기가 된다. 물론 원격 저장소에 저장해두면 캐시를 지워도 다시 pull해오면 되기 때문에 괜찮다. 오히려 로컬 캐시는 최대한 가볍게 유지하고 필요한 버전만 그때 그때 원격에서 가져오는 게 권장된다. 캐시 용량 정리 — dvc gc 되돌릴 수 없는 명령이므로, --dry 옵션으로 뭐가 지워질지 먼저 확인하고 실행하는 습관을 들이는 게 좋다. 리모트 스토리지 설정 기본 워크플로우 맺음말 link 방식들에 대해서 잘 모르는 상태로 무작정 했다가 원천 소스가 그대로 복사돼서 데이터 두 배 이벤트를 맛보았다. 공부는 역시 선행이 제 맛. 참조 DVC 정의 — DVC readme cache.type 기본값, 설정 방법 — Configuration | DVC 캐시 디렉토리 구조(md5 해시 저장 방식) — Internal Files | DVC 링크 방식별 특징, reflink의 copy-on-write 동작 — Large Dataset Optimization | DVC dvc checkout --relink 옵션 — checkout | DVC dvc gc 옵션 전체 — gc | DVC
공부
cover_image
On July 28, 2026머릿말 2026년 1차 정처기 필기/실기 한방에 통과한 사람 나야 나 ☆~(ゝ。∂) 공부하고 싶은데 자격증도 겸사겸사 따면 좋을 거 같아서 정처기 도전했다. 근데 하다보니 공부는 뒷전이고 효율의 한국인답게 기출풀개로 전락해버렸다. (출제 범위에 있는 개념 다 공부하면 올해 안에 시험 못 볼 거 같았음ㅠ) 시험 보고 나니 오히려 마음의 여유가 생겨서 이제는 정처기에 나오는 개념들에 대해 공부다운 공부를 해보려고 한다. 첫번째로 공부할 개념은 기출 최다 빈출 개념인 GoF 디자인 패턴이다. 시험볼 때는 패턴의 정의를 외우는 데에 급급했다면, 이번에는 이게 왜 필요한지, 언제 쓰는지, 이미 쓰고 있었는지에 집중해서 보고자 한다. 생성 패턴(Creational Patterns) 이란? 객체를 만드는 방법을 숨겨서, 객체가 어떻게 만들어지는지 신경 쓰지 않고 사용할 수 있게 하는 패턴들 Singleton 패턴 1) 정의 특정 클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 어디서든 그 인스턴스로 접근할 수 있는 통로를 제공하는 패턴 2) 언제 쓸까? 인스턴스가 여러 개 생기면 오히려 문제가 될 때 DB Connection Pool: 연결을 여러 개 만들면 리소스 낭비임 설정(config) 객체: 앱 전체가 같은 설정값을 봐야 하는데 인스턴스가 여러 개면 어떤 건 옛날 설정, 어떤 건 새 설정을 참조하는 불일치가 생김 로그 기록기(logger): 로그를 한 곳에 모아야 하는데 인스턴스가 흩어지면 로그도 흩어짐 3) 어떻게 쓸까? 위 코드에선 new Database()를 부를 때마다 매번 새 연결이 생긴다. 커넥션 비용이 비싸다고 가정하면 낭비이다. 예시가 DB여서 좀 애매한데 만약 각 클래스가 각자의 캐시 데이터를 갖고있으면 둘의 싱크가 안 맞아서 문제가 생길 수 있다. 싱글턴 패턴을 반영한 코드에서는 DB 인스턴스가 이미 만들어진 게 있으면 그걸 돌려주고, 없으면 새로 만든다. 그래서 코드 어디서 몇 번을 new Database() 해도 항상 같은 객체가 나온다. 4) 이미 어디서 써봤을까? 사실 JS 개발자는 이 패턴을 의식하지 않고도 매일 쓴다. a.js와 b.js가 각각 import해도 같은 instance를 받는다. 왜냐하면 JS에서 동일한 모듈은 처음 import될 때 딱 한 번 평가되고, 그 이후 어디서 import하든 이미 모듈 캐시에 저장된 export를 재사용한다. 브라우저의 window, document도 전역에 하나뿐인 Singleton이다. Factory Method 패턴 1) 정의 객체 생성의 구체적인 방법을 서브클래스에 위임하는 패턴 부모는 "객체가 필요하다"는 것만 알고, "정확히 어떤 타입의 객체인지"는 몰라도 된다. 제품 종류(객체)마다 전용 공장(서브 클래스)을 짓는다. → Factory Method 2) 언제 쓸까? 타입에 따라 분기해서 객체를 생성하는 코드가 있고, 그 타입이 계속 늘어날 가능성이 있을 때 cf. Factory Method vs Strategy 언뜻 보면 그때 그때 필요한 것을 외부에서 주입한다는 점에서 Strategy 패턴과 헷갈릴 수 있다. 하지만 Strategy가 "알고리즘 선택(= 행동)"의 문제라면, Factory Method는 "어떤 객체를 생성할지"의 문제이다. 3) 어떻게 쓸까? 위 코드에선 새 문서 타입이 생기면 Application 안의 openDocument를 다시 열어 고쳐야 한다. 이미 검증된 로직을 if 조건이 확실한지 확인하기 위해 다시 검증하기도 해야 한다. Factory Method 패턴이 적용된 코드에선 엑셀 지원을 추가하고 싶으면 Application은 한 글자도 안 건드리고 ExcelApplication extends Application { createDocument() { return new ExcelDocument() } }만 새로 추가하면 된다. 4) 이미 어디서 써봤을까? JS 스펙 자체에 Factory Method 패턴이 적용되어 있는 사례가 있다. Array.prototype.map 내부에서 new Array()를 호출하는 것이 아니다. 대신 원본 객체의 constructor[Symbol.species]를 참고하여 어떤 생성자를 사용할지 결정한 뒤 그 생성자로 새로운 결과 객체를 만든다. 즉, 부모 클래스(Array)가 제공하는 map 메서드는 결과 객체의 구체적인 타입 결정을 서브클래스에 위임하므로 Factory Method 패턴과 유사한 구조를 가진다. Array의 filter, slice 등과 Promise.prototype.then도 유사한 species 메커니즘을 사용한다. ⚠️ 최근에는 Symbol.species가 배열 생성 로직 중간에 임의의 코드를 끼워 넣어 메모리를 조작할 수 있는 틈을 제공할 수 있다는 보안 위험성이 제기돼 그냥 내장 객체로 반환하고자 하는 움직임이 있다. (참고 링크) Abstract Factory 패턴 1) 정의 관련 있는 여러 종류의 객체들을 "제품군" 단위로 생성할 수 있도록 객체 생성을 추상화하는 패턴 구체적인 제품 클래스와 생성 방법을 숨기면서 돌아가는 공장 cf. Factory Method vs Abstract Factory 둘 다 이름에 factory가 들어가서 함께 언급되긴 하는데 둘은 초점이 전혀 다른 곳에 맞춰져 있다. Factory Method : 객체 생성을 서브클래스에 위임하는 것에 방점 Abstract Factory : 선택한 계열에 따라 항상 같은 조합을 만들어주는 일관성에 방점 2) 언제 쓸까? 관련된 여러 객체가 반드시 같은 제품군으로 묶여야 할 때 환경/제품군이 바뀌어도 클라이언트 코드를 수정하지 않아야 할 때 3) 어떻게 쓸까? 위 코드에선 새 위젯이 늘어날 때마다 if (theme === 'dark') 분기가 새로 생긴다. 혹시라도 누군가 실수로 한 곳만 빼먹으면 DarkButton + LightCheckbox처럼 세트가 어긋난 상태가 조용히 만들어질 수도 있다. Abstract Factory 패턴이 적용된 위 코드에서 renderToolbar는 theme이 뭔지 모른다. 그냥 받은 factory가 dark든 light든 내부적으로 항상 짝이 맞는 세트를 돌려준다. 새 테마가 추가되어도 renderToolbar는 수정하지 않고 새 팩토리 클래스 하나만 추가하면 된다. 4) 이미 어디서 써봤을까? UI 프레임워크에서 테마별 컴포넌트 생성 시스템을 설계할 때 Abstract Factory 구조를 적용할 수 있다. 색상 테마라든가 OS 환경에 맞게 UI가 다르게 렌더링된다. (→ Concrete Factory가 여러 개다) 클라이언트는 안에 어떤 내용이 있는지 모른다. 항상 일관되게 같은 계열의 UI가 렌더링된다. Builder 패턴 1) 정의 복잡한 객체를 만드는 과정을 여러 단계로 쪼개서, 그 단계들을 조합해 최종 객체를 완성하는 패턴. 한 번의 생성자 호출로 다 끝내는 게 아니라 필요한 부분만 하나씩 붙여 나가다가 마지막에 완성하는 방식 2) 언제 쓸까? 생성자에 넘겨야 할 파라미터가 너무 많고, 그중 상당수가 선택적일 때 → 객체 생성 과정이 복잡할 때 완성되기 전까지 "중간 상태"를 만들면서 검증이 필요할 때 3) 어떻게 쓸까? 위 코드에선 호출부에서 인수의 순서를 외워 써야하고, 건너뛰는 인수는 undefined로 자리를 채워줘야 한다는 불편함이 있다. builder 패턴이 적용된 코드에선 필요한 내용만 호출하면 되고, 각 메서드 이름 자체가 "무슨 값인지"를 설명해줘서 호출부만 읽어도 뭘 하는지 바로 보인다. 또한 build() 시점에 해야 하는 필수값 검증도 한 곳에 모을 수 있다는 장점이 있다. 4) 이미 어디서 써봤을까? Knex.js / Mongoose 쿼리 빌더: knex('users').where('age', '>', 18).orderBy('name').limit(5) — 쿼리 조건을 단계별로 붙여나가다가 마지막에 실행한다. cf. 그럼 메소드 체이닝도 빌더 패턴인가? array.filter().map().reduce() 같은 배열 메소드 체이닝은 Builder가 아니다. 이건 "데이터를 단계별로 변형"하는 거지 "하나의 복잡한 객체를 조립"하는 게 아니기 때문이다. 체이닝의 형태를 띠고 this를 반환하는 것이 비슷해 보여도 목적이 다르다. 체이닝은 단순히 문법의 영역에 속하고, Builder 패턴은 여러 조각을 조립해 하나의 복잡한 객체를 완성하는 데에 의의가 있다. Prototype 패턴 1) 정의 새 객체가 필요할 때 인스턴스를 새로 만드는 대신, 이미 만들어져 있는 견본(prototype) 객체를 복제해서 만드는 패턴 cf. JS에서의 prototype JS 개발자라면 prototype, prototype chaining이라는 개념을 필수적으로 접하게 되는데 여기서 말하는 프로토타입은 GoF에서와의 개념과 조금 다르다. JS의 prototype: 사본이 원본에게 의존하는 관계 → 엄밀히 말해 복사가 아니라 위임 GoF 디자인 패턴의 prototype: 원본과 사본이 완벽하게 독립된 관계 → 복제 위 코드에선 dog가 animalProto의 현재 상태의 영향을 받는다. 아래 코드에선 dogClone이 animalProto와 독립된 객체기 때문에 animalProto의 내용이 변해도 dogClone이 영향 받지 않는다. 2) 언제 쓸까? 객체를 처음부터 만드는 비용이 큰데(ex. 무거운 초기화, 설정 파일 읽기, 계산 등), 비슷한 객체를 반복적으로 많이 만들어야 할 때 Factory 계열 패턴들처럼 "타입별로 서브클래스를 늘려가는 방식"이 부담스러울 때의 대안 만들어야 할 객체의 정확한 클래스/타입이 런타임 전엔 모를 때(JS한텐 별로 의미 없는 케이스) 3) 어떻게 쓸까? 위 코드에선 새 인스턴스가 생길 때마다 무거운 초기화 작업이 반복된다. prototype 패턴을 적용한 코드에선 고블린 프로토타입 인스턴스를 만들어두고 그걸 복사해서 각각의 고블린들을 커스터마이즈한다. 무거운 초기화 작업을 한 번만 하기 때문에 비용이 절약된다. 다만 stats를 { ...this.stats }로 복제하지 않고 그냥 this.stats로 참조를 공유했다면, goblin2.stats.hp = 50이 goblin1.stats.hp까지 같이 바꿔버렸을 거이다. Prototype 패턴에선 얕은 복사 vs 깊은 복사 문제를항상 조심해야 한다. 4) 이미 어디서 써봤을까? 게임 개발 : 몬스터/아이템 하나의 "마스터 템플릿"을 만들어두고, 실제 스폰할 때마다 그걸 복제해서 위치/체력만 바꾸는 방식 — Prototype 패턴의 가장 고전적인 예시 생성 패턴 요약 | 패턴 | 핵심 목적 | JS 실무 대표 활용 사례 | | :--- | :--- | :--- | | Singleton | 단 하나의 인스턴스 공유 | 전역 상태 관리, Logger, DB Connection Pool | | Factory Method | concrete class를 서브클래스로 위임하 여 객체 생성| document.createElement(), 커스텀 SDK | | Abstract Factory | 연관된 제품군 세트의 일관된 생성 | Cross-platform UI 컴포넌트, 테마 시스템 | | Builder | 복잡한 객체의 단계별 조립 | ORM 쿼리 빌더(Knex), HTTP Request Client | | Prototype | 무거운 인스턴스의 복제 생성 | 게임 객체 스폰, structuredClone 활용 |
공부
cover_image
On July 23, 2026서두 2026년 1차 정처기 필기/실기 한방에 통과한 사람 나야 나 ☆~(ゝ。∂) 뭔가 공부하고 싶은데 자격증도 겸사겸사 따면 좋을 거 같아서 정처기 도전했다. 근데 하다보니 공부는 뒷전이고 효율의 한국인답게 기출풀개로 전락해버렸다. (출제 범위에 있는 개념 다 공부하면 올해 안에 시험 못 봄 ㅠ) 우선은 응시해보는 거에 의의를 두고 기출 문제를 무작정 외우고 시험장 들어갔다. 솔직히 시험 치고나서 필기는 붙을 줄 예상했는데 실기는 진짜 천운으로 붙었다. 전혀 모르는 개념의 약어인데 그냥 한국어 설명을 영어로 번역해서 앞글자만 따서 맞춘 문제도 있었다 껄껄. 시험 보고 나니 오히려 마음의 여유가 생겨서 이제는 정처기에 나오는 개념들에 대해 공부다운 공부를 해보려고 한다. 첫번째로 공부할 개념은 기출 최다 빈출 개념인 GoF 디자인 패턴이다. 시험볼 때는 어떤 패턴인지 정의를 외우는 데에 급급했다면, 이번에는 이게 왜 필요한지, 언제 쓰는지, 이미 쓰고 있었는지에 집중해서 보고자 한다. 공부한 내용 Strategy 패턴 정의 알고리즘을 교체 가능한 단위로 캡슐화하는 패턴 즉, 하나의 목적을 위해서 여러 전략(Strategy) 을 바꿔 끼울 수 있게 만드는 패턴 언제/어떻게 쓸까? if/else나 switch를 써서 타입에 따라 다른 알고리즘을 처리하고 있고, 그 종류가 앞으로 계속 늘어날 가능성이 있을 때 써야 한다. 위 내용을 보면 새 할인 정책이 생길 때마다 calculate() 메서드 내부를 계속 고쳐야 한다. 기존 코드를 안 건드리고 새 알고리즘을 추가할 수 있어야 한다는 OCP(Open-Closed Principle) 원칙에 위배되기 때문에 Strategy 패턴을 써서 해결해보겠다. 아래는 Strategy 패턴 적용 후의 모습이다. Strategy 패턴이 적용된 코드에서는 PriceCalculator(Context)가 한 번도 수정된 적이 없다. 새 할인 정책은 discountStrategies 객체에 함수 하나 추가하는 것으로 끝난다. 이미 어디서 써봤을까? array.sort(compareFn) : 정렬 전략을 함수로 갈아끼우기 array.filter(predicate) / array.map(fn) : "어떻게 걸러낼지", "어떻게 변환할지"를 함수로 주입 즉, JavaScript에서 콜백 함수를 인자로 넘기는 습관 자체가 이미 Strategy 패턴이라고 볼 수 있다. Observer 패턴 정의 어떤 주체(Subject)의 상태가 바뀌었을 때, 그 변화에 관심 있는 여러 관찰자(Observer)들에게 자동으로 알려주는 패턴 하나의 변화가 여러 곳에 퍼지는 1:N 관계 언제/어떻게 쓸까? 한 곳의 상태 변화에 반응해야 할 대상이 여러 개이고, 그 개수·종류가 앞으로도 계속 늘어날 수 있을 때 사용하는 패턴이다. Subject 코드 안에 반응해야 할 대상들이 하드코딩되어 있고, 새 대상이 추가될 때마다 그 코드를 계속 고쳐야 한다면 Observer 패턴을 고려해야 한다. 주문이 완료되면 이메일 발송, SMS 발송, 로그 기록을 해야 한다고 가정해보자. OrderService가 EmailService, SmsService, LogService를 전부 직접 알고 있어야 하고, 강하게 결합되어 있다. '슬랙 채널 알림' 같이 새 알림 방식이 생기면 이 클래스를 또 열어서 고쳐야 한다. Strategy 때와 똑같이 OCP 위반 문제가 있다. 위에서 OrderService는 EmailService, SmsService가 뭔지 전혀 모르고, 목록에 있는 함수들을 순서대로 호출한다는 것만 안다. 새 알림 채널을 추가하고 싶으면 subscribe만 호출하면 끝이다. Strategy 때와 마찬가지로 기존 코드를 재수정할 필요가 없다. 이미 어디서 써봤을까? element.addEventListener('click', handler) : element(Subject)는 몇 개의 핸들러가 붙어있는지, 그 핸들러가 뭘 하는지 전혀 모른다. 그냥 클릭이 발생하면 등록된 모든 핸들러를 순서대로 호출한다. Node.js EventEmitter: Node.js에서 이미 만들어놓은 observer .on('event', listener) = subscribe .emit('event', ...args) = notify .off(eventName, listener) = unsubscribe Redux store.subscribe(listener): state가 바뀔 때마다 등록된 모든 리스너(주로 React 컴포넌트 리렌더 트리거)를 호출 Iterator 패턴 정의 컬렉션의 내부 구조가 어떻게 생겼는지 몰라도, 그 안의 요소들을 순서대로 하나씩 꺼내볼 수 있는 통일된 방법을 제공하는 패턴 핵심은 순회하는 방법과 컬렉션이 내부적으로 어떻게 저장돼 있는지를 분리하는 것 언제 쓸까? 컬렉션의 내부 구현을 몰라도 되게 만들고 싶을 때 서로 다른 자료구조 여러 개를 동일한 방식으로 순회하고 싶을 때 — 배열이든 Set이든 Map이든 for...of로 똑같이 돌 수 있는 것처럼 컬렉션 내부 구조가 나중에 바뀔 수 있는데 그때마다 그걸 순회하던 바깥 코드까지 다 같이 고치고 싶지 않을 때 어떻게 쓸까? 위 구조에서는 shelf.books라는 이름과 그게 배열이라는 사실까지 알아야 한다. 나중에 Bookshelf를 배열에서 Map이나 연결 리스트로 바꾸면 이걸 순회하던 외부 코드가 깨진다. 이제 외부 코드는 shelf.books라는 필드가 존재하는지조차 모르지만 for...of는 그냥 Symbol.iterator를 호출해서 { next() {...} } 형태의 객체(이터레이터)를 받고, next()를 반복 호출해서 { value, done }을 받는 약속(프로토콜) 만 지키면 된다. 이제 Bookshelf 내부를 배열 대신 Map이나 연결 리스트로 바꿔도, [Symbol.iterator] 메서드 내부만 고치면 되고 바깥의 for...of 코드는 바뀌지 않는다. 참고로 제너레이터 문법을 쓰면 훨씬 짧게 쓸 수 있다: 이미 어디서 써봤을까? for...of : 루프 자체가 Iterator 패턴 위에 세워진 문법 array.entries(), array.keys(), array.values() : 전부 이터레이터를 반환 제너레이터 함수(function*) : 이터레이터를 쉽게 만들게 해주는 JS 문법 const arr = [...shelf]; : 스프레드 연산자 const [first] = shelf; : 구조 분해 할당 Array.from(shelf); : 배열이 아닌 것을 진짜 배열로 변환해주는 함수 Command 패턴 정의 요청을 객체로 캡슐화해서 실행부와 분리하는 패턴 패턴의 이름이 command인 이유는 군대에서의 상명하복처럼 실행하는 측은 요청이 뭔지 몰라도 무조건 실행하기 때문이다. (그렇다고 요청과 실행 사이에 위계질서가 있다는 뜻은 아님) Cf. Strategy와 헷갈리는 지점 Strategy와 Command 패턴 둘 다 동작을 함수/객체로 캡슐화해서 들고 다닌다라는 개념이 공통돼서 헷갈릴 수 있다. 하지만 두 패턴이 방점을 찍은 지점이 다르다는 것을 알면 헷갈리지 않을 수 있다. - Strategy: "이 알고리즘들 중 어떤 걸로 할지 고르는 것"이 목적으로, Context는 전략 하나를 골라 즉시 실행한다. - Command: "이 요청 자체를 나중에 실행하거나, 여러 번 실행하거나, 되돌릴 수 있게" 포장하는 게 목적으로, 반환값보다 실행 시점을 미루는 것 / 실행 이력 / 되돌리기 능력이 핵심이다. 언제 쓸까? 누가 무슨 요청했는지와 그 요청을 어떻게 실행/취소하는지를 분리하고 싶을 때 - "실행"과 "실행 취소"를 함께 관리해야 할 때 - 요청을 즉시 실행하지 않고 큐에 쌓았다가 나중에 순서대로 실행해야 할 때 - 실행된 작업들의 이력을 남기고 재실행해야 할 때 - 여러 명령을 하나로 묶어서 한 번에 실행해야 할 때 어떻게 쓸까? 위 코드에서 방금 한 작업만 취소하고 싶다면 editor 안에 히스토리 스택을 직접 넣고, undo 로직을 editor 내부에 추가해야 한다. 수정된 코드에서 TextEditor는 "undo"라는 개념을 몰라도 된다. undo 기능은 각 AddTextCommand 객체가 들고 있고, CommandManager는 그걸 순서대로 쌓았다 꺼내는 일만 한다. 이미 어디서 써봤을까? command line : - 셸 소스코드 안에는 요청 로직이 없고(cli 프로그램 따로 설치해야 됨) - 실행 후에도 이력이 남아있고 - 다시 실행 가능하며 - 여러개를 묶어서 하나로 실행할 수 있는 전형적인 command 패턴 Redux의 action : - action(요청 객체) → dispatch → reducer(실행) - 요청과 실행이 분리됨 텍스트 에디터 : 사용자가 과거에 한 요청의 이력을 쌓고 연속적으로 실행취소할 수 있음 후기 존경하는 AI 선생님께 GoF 디자인 패턴을 사사했는데 내용이 너무 많다보니 handoff.md를 작성해놓고 계속 세션을 새로 생성하면서 대화를 이어 나갔다. 어차피 내가 한 대화 기록일 테니 따로 handoff.md 파일을 읽어보지 않았는데 어쩌다 열어보니 이런 내용이 있더라. 사용자 학습 스타일 메모 정의 암기보다 "왜 필요한가"에 집중하길 원함 Before/After 코드 비교로 이해하는 걸 선호 이미 알고 있는 JS 관용구와 연결지어 설명하면 이해가 빠름 개념이 조금이라도 애매하면 바로 "이게 무슨 말이야?" 하고 파고드는 스타일 — 세밀하고 구체적인 재설명을 원함, 두루뭉술한 답변은 피할 것 직전 답변에서 쓴 특정 문장/단어를 그대로 인용하며 되묻는 경우가 많음 — 답변 시 그 원문 표현을 다시 가져와서 정확히 대응시켜 설명할 것 새 패턴을 배울 때 "이거 사실 전에 배운 패턴이랑 같은 거 아니야?"처럼 스스로 유사점을 찾아 비교하려는 경향이 있음 — 틀렸을 때 무조건 아니라고 하지 말고, 어느 지점까지는 맞는 관찰인지 인정한 뒤 정확한 차이(의도/목적)를 짚어줄 것 선생님 제가 귀찮으셨던 건 아니죠. 멍청해서 죄송해요 ㅠㅠ 덕분에 아무 죄책감과 쪽팔림 없이 바보 질문을 남발했습니다. 탄생해주셔서 감사합니다. 정처기 볼 때는 정의랑 패턴 이름이 도대체 무슨 연관이 있는지 모르겠어서 외우기 힘들었는데 역시 개념을 조금이라도 이해해야 외우기도 좀 더 수월하다. 다음에는 구조 패턴 공부해야징~
공부
cover_image
On July 21, 2026계획 게시글 동적 메타데이터 생성 루트 메타데이터 보강 robots.txt sitemap.xml 동적 생성 JSON-LD 구조화 데이터 Favicon / App Icon 게시글 작성 시 description 입력란 추가 구현 메타데이터 가장 눈에 띄는 문제는 모든 게시글 상세 페이지가 똑같은 title/description을 내보내는 것이다. Next.js의 동적 메타데이터 생성 기능을 붙여서 각 글의 실제 제목과 본문 앞부분(HTML 태그를 걷어내고 150자 정도)을 description으로 자동 추출하도록 만들었다. OG 태그, Twitter Card, canonical URL도 같이 채웠다. 루트 레이아웃에도 기본 title/description, OG 기본값, 크롤러가 사이트 구조를 파악할 수 있도록 robots.txt와 sitemap.xml을 동적으로 추가했다. 구조화 데이터(JSON-LD) 일반 HTML만 있으면 검색엔진은 "이게 블로그 글인지, 언제 썼는지, 누가 썼는지"를 텍스트에서 추측해야 한다. 으로 스키마를 명시하면 이 추측을 없앨 수 있다. 먼저 게시글 스키마(제목/설명/작성일/수정일/작성자/썸네일)를 넣었고, 이어서 이동 경로(홈 › 게시판 › 글제목) 스키마를 추가했다. 후자의 실질적 효과는 검색 순위 상승이 아니라 검색 결과에 URL 그대로가 아닌 "게시판 › 글제목" 같은 자연어 경로가 표시되게 만들어 클릭률을 높이는 쪽에 있다. 여기서 배운 게 있다면 지금까지 넣은 구조화 데이터가 전부 "글 단위" 정보였고 "사이트 단위" 정보가 없었다는 점이다. 브랜드 이름 자체로 검색하는 상황에서는 검색엔진이 사이트명/운영자를 텍스트를 통해 추측하는 게 아니라 명시된 내용으로 인식할 수 있도록 루트에 사이트 + 운영자 스키마를 채워 넣었다. 아이콘 — favicon, app icon, manifest 처음에는 그냥 파비콘만 설정하면 되나 싶었는데 아이콘 종류마다 쓰이는 곳과 요구사항이 다 다르다는 사실을 알게 되었다. favicon: 브라우저 탭 아이콘. 작은 사이즈, .ico (🤔 왜 파비콘은 ico 확장자를 주로 쓰냐고 물어봤는데 요즘 시대에는 관습적으로 쓴다고 한다). 앱 아이콘: "홈 화면에 추가"나 PWA 설치 시 쓰이는 훨씬 큰 정사각형 이미지. 플랫폼마다도 보는 소스가 다르다는 걸 하나 하나 부딪히며 확인했다. Android Chrome은 이미지 파일 자체가 아니라 Web App Manifest(manifest.json)의 icons 목록만 본다. 아이콘 이미지를 아무리 정해진 이름으로 넣어도 매니페스트 파일이 없으면 "홈 화면에 추가"에서 그냥 기본 아이콘으로 대체된다. iOS Safari는 투명 배경이 있는 터치 아이콘을 제대로 처리하지 못하는 경우가 흔하다. 실제 페이지 소스를 직접 열어서 에 원하는 아이콘 태그들이 다 붙는지 확인하고 나서야 마무리할 수 있었다. 게시글 URL에 제목 붙이기 순수 ID만 포함된 URL(/board/{id})은 사람이 URL만 보고 콘텐츠를 유추하기 어렵고, 검색엔진 역시 URL 내 키워드를 활용해 페이지 내용을 파악하기 어렵다. 이를 개선하기 위해 /board/{id}-{제목-슬러그} 형태로 변경했다. 동적 라우팅을 할 때 {id}-{제목-슬러그}에서 id를 자동으로 추출해주는 것은 아니다. 예를 들어 /board/123-게시글-제목이라는 요청이 들어오면 123-게시글-제목 전체가 파라미터로 전달된다. 따라서 이 통짜 문자열에서 실제 게시글 ID를 분리하는 작업은 따로 해야 한다. OG 태그 OG 태그 설정을 안 해놓으니까 썸네일이 없는 글을 메신저나 SNS에 공유하면 미리보기 이미지가 아예 안 떴다. 기본 이미지를 fallback으로 넣었는데 처음 넣은 이미지가 세로형이라 소셜 공유가 기대하는 가로형 비율(1200×630 근처)에 맞추면서 어색하게 잘렸다. 이미지를 새로 그리는 대신, 같은 이미지를 배경색으로 여백을 채워(레터박싱) 원하는 비율로 재가공하는 쪽을 택했다. 방문자 분석 애널리틱스 측정 ID는 이미 발급받아 환경변수에 있었는데, 실제로 이 값을 사용하는 코드가 없었다. 공식 통합 패키지로 연결하고, 로컬 개발 트래픽이 실제 데이터에 섞이지 않도록 프로덕션 환경에서만 로드되게 했다. 검색 노출을 보여주는 도구(Search Console류)와 방문자 행동을 보여주는 도구(애널리틱스)는 역할이 다르다. 전자는 "어떤 검색어로 얼마나 노출/클릭됐는가", 후자는 "실제로 들어온 사람이 뭘 봤는가"를 알려준다. 이 둘을 같이 봐야 "어떤 검색어로 들어와서, 어떤 글을 오래 읽었는지"까지 파악할 수 있다. 작성자가 직접 쓰는 SEO 설명 본문에서 자동 추출한 description은 글의 첫머리가 뭐냐에 따라 자칫 내용이 어색해질 수 있어서 글쓰기/수정 화면에 별도의 설명 입력란 추가했다. 메타데이터와 구조화 데이터 둘 다 이 값을 우선 쓰고, 없으면 기존처럼 본문에서 자동 추출하도록 순서를 정리했다. 정적 캐싱과 fetch 캐싱(계획에 없던 작업) 게시글 목록 페이지를 방문할 때마다 매번 처음부터 새로 렌더링되고 있었다. 캐싱 없는 서버 렌더링은 느리고 페이지 속도는 검색 랭킹에도 영향을 주는 요소라 주기적으로 캐시를 재생성하는 방식(ISR)으로 바꾸기로 했다. 시도 1: revalidate 설정만 추가하기 — 실패 처음에는 동적 렌더링을 강제하던 설정을 제거하고 재생성 주기만 추가하면 ISR이 적용될 것이라고 생각했다. 하지만 빌드는 정상적으로 완료됐음에도 실제 페이지는 여전히 요청마다 새로 렌더링되고 있었다. 확인 결과, 페이지 코드 자체가 정적 생성 조건을 만족하지 못하는 구조였다. 실패의 원인은 아래와 같다. 서버 컴포넌트에서 URL 파라미터를 직접 읽고 있었다. 서버 컴포넌트가 요청마다 달라질 수 있는 값을 참조하면, Next.js는 해당 페이지의 결과가 요청에 따라 달라진다고 판단한다. 따라서 해당 페이지를 미리 생성해 캐싱할 수 없게 되고 매 요청마다 서버에서 새롭게 렌더링한다. 이를 해결하기 위해 검색·필터 조건 처리 부분은 클라이언트 컴포넌트로 이동시켰다. 클라이언트 컴포넌트는 이미 생성된 HTML을 브라우저에서 실행한 뒤 URL 상태를 읽어 화면을 변경하기 때문에, 서버가 생성하는 페이지 결과 자체는 고정된 상태로 유지할 수 있다. 결과적으로 게시글 목록 영역은 정적으로 캐싱하고, 검색과 필터 같은 사용자 인터랙션만 클라이언트에서 처리하는 구조로 변경했다. ISR은 빌드 단계에서 페이지를 미리 생성한다. 이때 실행되는 코드에서 자신의 API 주소를 호출하면 요청을 처리할 서버가 없어 실패한다(그것이 빌드 단계이기 때문에...!). 그래서 서버 컴포넌트에서는 API를 거치지 않고 데이터베이스를 직접 조회하도록 변경했다. fetch 캐싱과 페이지 단위 캐싱은 다른 레이어다 이 과정에서 정리된 핵심 개념 하나, Next.js엔 캐시가 두 겹이 있다. | | fetch 단위 캐싱 | 페이지(라우트) 단위 캐싱 | | --- | ----------- | -------------- | | 캐싱 대상 | 개별 요청 하나의 응답 데이터 | 라우트 전체의 렌더링 결과 | | 범위 | 함수 단위 | 페이지 단위 | 페이지 캐싱을 설정했다고 해서 무조건 페이지가 캐싱되는 것은 아니다. 페이지 안에서 사용하는 fetch도 함께 캐시될 수 있어야 한다. 만약 fetch 중 하나라도 항상 최신 데이터를 가져오도록(cache: 'no-store' 등) 설정되어 있다면, Next.js는 그 페이지 전체를 동적으로 렌더링한다. 페이지는 여러 fetch의 결과를 모아 만들어지기 때문에, 그중 하나라도 매번 새로 가져와야 한다면 완성된 페이지도 캐시할 수 없기 때문이다. 공식 문서에서도 아래와 같이 언급하고 있다. 출처: Next.js 공식 문서 진짜 캐싱되는지 확인하는 법 브라우저로 보면 새 글이 목록에 바로 뜨는데 이건 캐싱이 안 된다는 증거가 아니다. 화면이 뜨자마자 클라이언트가 즉시 재조회하도록 되어 있으면, 서버 캐시가 낡아 있어도 화면엔 항상 최신이 보인다. 실제로 서버 쪽 캐싱이 동작하는지는 응답 헤더(캐시 히트 여부를 알려주는 헤더, 설정한 재생성 주기와 일치하는 max-age 값)와 응답 속도(캐시 히트면 밀리초 단위, DB 재조회면 그보다 느림)로 확인해야 한다. 글을 쓰자마자 JS 실행 없이 서버가 내려주는 순수 HTML만 확인하면, 설정한 재생성 주기 안에는 새 글이 안 보여야 정상이라는 것도 검증 포인트였다. 검색엔진에 존재를 알리기 위에는 "크롤러가 이해하기 좋게 코드를 만드는" 작업이었고, 이걸 실제로 검색엔진에 등록하는 건 완전히 다른 단계이다. 나는 구글 서치 콘솔과 네이버 서치 어드바이저에 등록했다. 구글 서치 콘솔 등록 과정에서 알게 된 사실! 최초에 사이트를 등록할 때 메타 태그를 넣지도 않았는데 소유권이 자동으로 인증돼서 처음엔 이미 연결한 애널리틱스 덕분인가 했다. 실제로 확인해보니 그게 아니라 DNS를 관리하는 업체가 검색엔진과 공식으로 연동돼 있어서, 로그인만으로 검색엔진이 API를 통해 소유권 확인용 레코드를 대신 추가해주는 정식 지원 기능 때문이었다. 그래서 인증 후에도 그 레코드를 실수로라도 지우면 안 된다는 엄중한 경고를 받았다. 후기 그동안 검색 엔진에 노출이 중요하지 않는 백오피스 앱을 만드는 일이 잦았고, 나에게까지 SEO 최적화 임무가 오지 않아서 직접 이것저것 해볼 일이 드물었다. 하고자 하면 더 할 수 있는 게 산더미겠지만 이걸로 첫 삽을 뜬 것으로 해보겠다.