안녕하세요! 그로스로그 입니다!😊 🌱
파서가 위치를 모르는 이유, index가 순환참조를 부르는 이유, R이 아직 쓰이는 이유. 셋 다 설계 목적을 되짚습니다.
5기 4회차입니다. 도구를 쓰다 막힌 자리에서 "이건 원래 무엇을 하려고 만든 건가"로 돌아간 세 편이 모였어요.
01
🧾 [Swift/iOS] (Base) XML Parser를 만드는 과정, 설계부터 고민까지
주한솔 님
API가 XML로 응답하는 경우가 늘면서, 매번 커스텀 파서를 만드는 대신 재사용 가능한 XML → Codable 파서를 직접 설계한 기록입니다.
방식 세 가지를 검토하고 하나를 고릅니다. subscript ·
[String: Any] 변환 · XML → JSON → Codable. 각각을 왜 기각했는지도 적어요.후반부가 트러블슈팅입니다. struct와 class 사이의 선택, 루트 태그 처리 실수, 그리고
lazy var와 struct 조합에서 만난 컴파일 에러까지.라이브러리를 쓰지 않은 이유도, 특정 방식을 고른 이유도 다 적혀 있는 글입니다.
조금 더 들어가 보면
시작이 좋습니다. Swift 기본 XMLParser가 왜 불편한지를 설계 목적에서 설명해요.
특정 태그의 정보를 담을 프로퍼티를 만들고, 플래그를 세우고… 그런 작업이 붙는다는 겁니다. 그리고 부모–자식이나 형제 관계도 알기 어렵다고요. "그저 파싱 중인 한 태그, 한 태그의 정보만 전달해줄 뿐이죠."
이게 결함이 아니라 스트리밍 파서의 성격입니다. 문서 전체를 메모리에 안 올리는 대신 지금 이 순간만 알려주는 구조예요. 그래서 재사용이 어렵고 매번 커스텀 파서를 만들게 된다는 진단이 나옵니다.
🔀 세 가지 방식과 기각 이유
대안을 셋 놓고 각각 판단합니다.
· subscript 방식 — SWXMLHash(스타 1.5k)를 봤는데 사용 예시가 xml["user"]["items"][0]["bag"].value였습니다. "직관적이지만 하드코딩을 사용하는 방식이었습니다. 바로 패스했습니다"
· [String: Any] 변환 — 기존에 쓰던 방식인데 "여러 구조로 넘어오는 XML을 다 처리하기에는 쉽지 않을 것 같았습니다"
· Codable 방식 — XML → JSON → Codable. 변환 레이어가 하나 늘지만 쓰는 쪽은 JSONDecoder와 거의 동일해집니다
세 번째를 고른 기준이 명확합니다. 사용하는 쪽의 경험이에요. 다른 API를 다룰 때와 같은 모양으로 쓸 수 있느냐를 봤습니다.
그리고 설계 순서가 인상적입니다.
XMLNode(트리 노드) → XMLDocumentParser(노드 트리 생성) → XMLNode+JSON(JSON 변환) → XMLDecoder(외부 인터페이스). 바깥에서 안으로 정한 것입니다.
🧊 struct로 바꾸면서 생긴 일
XMLNode를 struct로 할지 class로 할지 고민한 대목이 값 타입 공부가 됩니다.
struct XMLNode {
let name: String
var value: String = ""
var attributes: [String: String] = [:]
var children: [XMLNode] = []
}
처음엔 class로 만들었다고 해요. 트리에서 자식을 추가할 때 참조로 접근하면 편하니까요. 그런데 파싱이 끝난 뒤엔 불변 데이터에 가깝고 외부에서 공유할 필요가 없어서 struct로 바꿉니다.
바꾸니 파서 코드가 따라 바뀌어야 했습니다.
// class일 때 - 참조라서 직접 수정 가능 stack.last?.children.append(node) // ❌ struct에선 복사본 반환 // struct일 때 - 인덱스로 직접 접근해야 함 stack[stack.count - 1].children.append(node) // ✅
stack.last가 복사본을 반환하니 append해도 원본에 반영되지 않는다는 것. 값 타입에서 자주 걸리는 자리입니다.
📝 원문에서는 이렇게 고쳤습니다
트러블슈팅 두 개가 실제 재료입니다.
하나, 루트 태그가 한 겹 더 씌워진 문제.
// 잘못된 코드 let jsonData = try root.toJSONData() return try jsonDecoder.decode(type, from: jsonData)
toJSONData()는 디버깅 용도라 루트 노드 이름을 키로 감싸서 반환합니다. <items> XML이면 { "items": { "item": [...] } }가 되죠. 그런데 Items 구조체는 최상위에서 item을 찾으니 디코딩이 항상 실패합니다.
// 수정 후
private func decode<T: Decodable>(_ type: T.Type, from root: XMLNode) throws -> T {
let jsonData = try JSONSerialization.data(withJSONObject: root.toJSONObject())
return try jsonDecoder.decode(type, from: jsonData)
}
toJSONObject()는 루트 이름 없이 내용만 반환해서 구조체와 정확히 맞습니다. 그리고 여기서 규칙이 하나 정리돼요 — 루트 태그명은 decode() 호출 시 타입으로 표현하고, struct는 그 내용물을 정의한다.
둘, lazy var와 struct의 충돌.
private lazy var xmlDecoder: any XMLDecoding = XMLDecoder()
이게 struct의 non-mutating 함수 안에서 컴파일 에러를 냈습니다. 원인 설명이 정확합니다.
lazy var는 처음 접근할 때 값을 초기화하고 self에 저장한다. 즉 getter가 내부적으로 self를 변경한다.""
func fetch()는 non-mutating이라 self가 immutable하고, self가 immutable하니 lazy var의 mutating getter를 호출할 수 없는 것입니다."읽기처럼 보이는 접근이 사실은 쓰기였던 겁니다. 해결은 담백해요 — "생성 비용이 크지 않기 때문에 lazy를 삭제했습니다."
큐레이터 노트
기각한 선택지와 그 이유를 남긴 글이라 뽑았습니다.
무언가를 직접 만든 기록은 대개 완성된 결과부터 보여줍니다. 이 글은 왜 라이브러리를 안 썼는지, 세 방식 중 둘을 왜 버렸는지를 먼저 적어요. SWXMLHash를 "스타 1.5k로 준수했다"고 인정하면서도 하드코딩 방식이라 패스했다고 밝히는 대목이 특히 그렇습니다. 선택이 아니라 판단의 기록이 됩니다.
"최종 사용 형태를 먼저 정하고 역으로 설계했다"는 순서도 좋았습니다. 파서 내부 구조부터 잡으면 쓰기 어려운 API가 나오기 쉬운데, 쓰는 모습을 먼저 그려두면 그 반대가 됩니다.
트러블슈팅에서 개념으로 내려간 것이 이 글의 깊이입니다. lazy var 에러를 "이렇게 하면 됩니다"로 끝내지 않고 왜 getter가 mutating인지까지 갔어요. 마무리 문장이 그걸 요약합니다.
lazy var의 내부 구조, Dictionary의 순서 보장 여부 같은 개념들을 다시 한번 짚어볼 수 있었습니다."02
🧰 FSD 아키텍처 정독하기 4편
이승민 님
FSD를 실제로 적용하면서 만난 주의점과 최적화를 정리한 4편입니다. 1편이 개념이었다면 이번은 함정 쪽이에요.
큰 UI 블록을
shared가 아니라 widgets에 둬야 하는 이유를 레이어 참조 제약으로 설명하고, 코드로 대비시킵니다.후반부는
index(public API)가 만드는 문제 세 가지 — 순환참조, tree shaking 실패, HMR 성능 저하. 그리고 각각의 해결책과 그 해결책의 한계까지.편의를 위해 둔 장치가 어떻게 비용이 되는지를 다룹니다.
조금 더 들어가 보면
첫 주제가 실무에서 바로 갈리는 판단입니다. 모든 페이지에 쓰이는 헤더를 어디에 둘 것인가.
재사용이니 shared가 떠오릅니다. 그런데 헤더가 로그인 버튼·장바구니 미리보기·유저 아바타를 담고 있다면 얘기가 달라져요. 그 요소들은 비즈니스 로직을 갖고 있어서 features나 entities에 있고, shared는 상위 레이어를 참조할 수 없습니다.
// widgets/header/ui/Header.tsx 올바른 방법
import { LoginButton } from 'features/auth'; // 하위 레이어
import { CartPreview } from 'features/cart'; // 하위 레이어
import { UserAvatar } from 'entities/user'; // 하위 레이어
import { Button } from 'shared/ui'; // 하위 레이어
export function Header() {
return (
<nav>
<Logo /><CartPreview /><UserAvatar /><LoginButton />
</nav>
);
}
"재사용되니까 shared"가 아니라 "무엇을 알고 있느냐"로 자리가 정해집니다. 로고와 링크만 있는 헤더는 shared에 둘 수 있지만, 로그인 상태를 아는 순간 widgets으로 올라가야 해요.
무엇이 features·entities에 가야 하는지도 코드로 구분합니다.
// features/auth/ui/LoginButton.tsx
// shared에 넣으면 안 됨 - 로그인 로직을 알고 있음
import { useAuth } from '../model/authStore';
// entities/user/ui/UserAvatar.tsx
// shared에 넣으면 안 됨 - User라는 도메인 개념을 알고 있음
import { useCurrentUser } from '../model/userStore';
주석 한 줄씩이 판단 기준입니다 — 로직을 아는가, 도메인을 아는가.
🔁 index가 순환참조를 부르는 경로
여기부터가 이 편의 핵심입니다. index(public API)는 FSD의 편의 장치인데, 그게 함정을 만들어요.
index.js (public API) ─────────┐ │ │ ↓ HomePage를 export │ ui/HomePage.jsx │ │ │ ↓ "../" 에서 import │ 다시 index.js로! ──────────────┘
index.js가 HomePage를 내보내는데, 그 HomePage가 다시 "../"(즉 index.js)에서 다른 걸 가져오면 원이 만들어집니다. 글이 짚은 대로 "순환 참조는 인덱스 파일이 없더라도 발생할 수 있지만 인덱스 파일을 쓰면 이런 실수가 발생하기 쉬워진다"는 겁니다.
해결이 한 줄 규칙으로 정리됩니다.
// 같은 슬라이스 내부 → 직접 경로
import { loadUserStatistics } from "../api/loadUserStatistics";
// 다른 슬라이스 → public API (alias 사용)
import { Button } from "@/shared/ui";
import { UserCard } from "@/entities/user";
index는 바깥을 향한 문이지 안에서 돌아다니는 복도가 아니라는 것입니다.
📦 큰 index가 번들을 부풀리는 이유
두 번째 문제는 tree shaking입니다.
shared/ui 아래 button · text-field · carousel · accordion이 있고, 이걸 전부 묶은 큰 index가 있다고 해봅시다. 외부에서 Button 하나만 쓰고 싶은데 carousel과 accordion의 무거운 의존성까지 번들에 딸려 올 수 있습니다.
글이 조건을 정확히 답니다. "대부분의 public API에서는 모듈간 연관성이 높아서 큰 문제는 없으나 shared/ui, shared/lib처럼 서로 관련성이 낮은 모듈 묶음에서는 문제가 커진다." 모든 index가 문제가 아니라, 관련성 낮은 것들을 한 문에 몰아넣은 index가 문제라는 것이죠.
해결은 문을 잘게 만드는 것입니다.
📂 shared/ui/ 📂 button/ index.ts 📂 text-field/ index.ts
import { Button } from "@/shared/ui/button";
import { TextField } from "@/shared/ui/text-field";
📝 원문에서는 한계까지 적습니다
여기서 그치지 않고 이 해결책이 완전하지 않다고 덧붙입니다.
IDE의 자동 import가 규칙을 우회한다는 지적이 실무적입니다. 사람이 지키기로 마음먹어도 도구가 대신 어길 수 있어요.
세 번째 문제는 개발 경험입니다. index가 너무 많아지면 불필요한 모듈이 딸려 들어와 개발 서버 실행 속도와 HMR 성능이 떨어집니다. 최적화 방법을 넷으로 정리했어요.
· shared/ui·shared/lib의 큰 index를 없애고 모듈 단위로 쪼갠 작은 index를 쓴다
· 세그먼트 단위로 불필요한 index를 만들지 않는다. features/comments/index.ts가 이미 있으면 features/comments/ui/index.ts는 필요 없다
· 큰 프로젝트는 기능 단위 chunk나 패키지로 나눈다. Google Docs의 Document Editor와 File Browser처럼요
· 모노레포에서는 각 패키지를 독립적인 FSD root로 구성한다. 어떤 패키지는 shared·entities만, 다른 패키지는 app·pages만 갖는 식으로
큐레이터 노트
규칙이 아니라 규칙의 부작용을 다룬 글이라 뽑았습니다.
1편이 FSD가 무엇인지였다면 4편은 그걸 실제로 깔았을 때 어디가 아픈지입니다. 그리고 아픈 지점이 전부 편의를 위해 둔 장치에서 나와요. index는 내부를 감추려고 만든 건데 순환참조를 부르고, 한 문에 몰아넣으면 번들이 커지고, 문이 너무 많으면 개발 서버가 느려집니다.
해결책의 한계를 같이 적은 것이 이 글의 신뢰를 만듭니다. 작은 index로 쪼개라고 해놓고 "IDE auto import가 public API 규칙을 깰 수 있다"고 덧붙였어요. 완전히 막을 수는 없고 관리해야 하는 문제라고 성격을 정확히 규정한 것입니다.
연재 4편째라는 것도 짚어두고 싶습니다. 1편(개념) → 4편(주의점과 최적화)으로 오면서 글의 성격이 바뀌었어요. 공식 문서 정리에서 적용 경험 정리로 옮겨왔습니다. 한 기수 안에서 한 주제를 네 번 파고들면 이런 궤적이 남습니다.
03
📊 R은 왜 아직도 살아남았는가
황대웅 님
AI·머신러닝 생태계가 Python 중심으로 굳어진 지금, 왜 여전히 R을 쓰는 사람들이 있는가를 다룬 글입니다.
답의 출발점이 R이 범용 언어로 만들어지지 않았다는 사실입니다. 웹 서버도, 게임 엔진도, 운영체제도 아닌 "데이터를 분석하기 위해" 만들어졌다는 것.
Python과 R을 경쟁이 아니라 역할 분담으로 놓고, AI 시대에 R이 살아남을 수 있을지까지 전망합니다.
"이제 R은 끝난 언어 아닌가요?"라는 질문에 정면으로 답하는 글입니다.
조금 더 들어가 보면
핵심 논증이 설계 목적에 있습니다.
좁게 만들어진 것이 약점이 아니라 남은 이유라는 뒤집기입니다. Python은 범용성에서 압도적이지만, R은 "데이터를 '해석'하는 과정 자체에 굉장히 최적화되어" 있다고요.
그리고 그걸 코드 흐름으로 보여줍니다.
sales <- read_csv("sales.csv")
filtered <- sales %>%
filter(region == "Seoul")
summary <- filtered %>%
group_by(product) %>%
summarise(revenue = sum(amount))
ggplot(summary, aes(product, revenue)) +
geom_col()
불러오고, 거르고, 묶고, 그리는 흐름이 파이프(%>%)로 이어집니다. 글이 짚은 요점은 문법의 쉬움이 아니에요.
🤝 경쟁이 아니라 역할 분담
두 번째 장이 실무 감각입니다. 현업에서 두 언어가 싸우는 게 아니라 나눠 맡는다는 것.
| Python의 자리 | R의 자리 |
|---|---|
| 모델 서빙 · API 구축 | 통계 분석 · EDA |
| 머신러닝 파이프라인 | 실험 데이터 해석 |
| AI 애플리케이션 개발 | 학술 연구 · 데이터 리포팅 |
연구 분야에서 R의 존재감이 큰 이유도 답니다. 바이오 통계, 의학 연구, 금융 분석, 사회과학에서 여전히 많이 쓰이는데, "R은 통계 모델링과 분석 워크플로우가 굉장히 깊고 성숙해 있기 때문"이고 수십 년 축적된 패키지와 연구 생태계가 진입 장벽이 된다고요.
그리고 하나를 더 짚습니다. "재현 가능한 분석(reproducible analysis)" 문화가 강하다는 것. 이건 언어 성능으로 대체하기 어려운 항목입니다.
📝 원문에서는 이렇게 전망합니다
마지막 질문이 "LLM과 AI 시대에도 R은 살아남을 수 있을까"입니다. 답이 균형 잡혀 있어요.
AI 모델 개발의 중심은 앞으로도 Python일 가능성이 높다고 인정합니다. PyTorch·TensorFlow·Hugging Face·LangChain이 이미 Python 중심으로 굳었으니까요. 그런데 결론이 그 뒤에 옵니다.
모델을 만드는 것만큼이나 결과를 설명하고, 데이터를 탐색하고, 이상치를 발견하고, 통계적으로 검증하고, 시각적으로 전달하는 과정이 중요해진다는 겁니다. 그리고 바로 거기가 R이 강한 자리라고요.
맺음이 이 글의 관점을 압축합니다.
큐레이터 노트
인기 순위가 아니라 설계 목적으로 답한 글이라 뽑았습니다.
"어떤 언어가 살아남을까" 류의 글은 대개 점유율과 채용 공고 수로 갑니다. 이 글은 왜 만들어졌는지를 근거로 삼아요. 범용 언어가 아니었다는 사실을 약점이 아니라 생존 이유로 뒤집는 대목이 이 글의 중심입니다.
경쟁 구도를 걷어낸 것도 좋았습니다. "R vs Python"으로 시작해서 역할 분담으로 착지합니다. Python으로 모델을 만들고 R로 분석과 리포팅을 하는 흐름이 자연스러워졌다고요. 이기고 지는 문제가 아니라 어디에 두느냐의 문제로 옮겨놓은 것입니다.
AI 시대 전망을 반대 방향으로 낸 것이 인상적이었습니다. AI가 강해지면 분석이 필요 없어질 것 같은데, 오히려 결과를 해석하고 검증하는 일의 비중이 커진다고 봤어요. 이 회차 앞뒤 글들이 "AI가 만든 것을 누가 검증하는가"를 다룬 것과 정확히 겹칩니다.
기술 선택을 고민하는 회원들에게 도움이 될 기록입니다. "인기 있는 것"과 "이 일에 맞는 것"이 다를 수 있다는 걸 한 언어의 사례로 보여주니까요.
💡 세 편을 겹쳐 읽으며
Swift 파서, 프론트엔드 폴더 구조, 통계 언어. 접점이 없어 보이는데 세 글이 같은 방식으로 문제를 풉니다.
"이건 원래 무엇을 하려고 만든 건가"로 돌아간다
주한솔 님은 XMLParser가 위치를 모르는 걸 결함으로 보지 않았습니다. 스트리밍 파서의 성격이니까요. 그래서 그 위에 트리를 쌓는 층을 따로 만들었습니다.
이승민 님은 index가 순환참조를 부르는 이유를 같은 방식으로 짚습니다. index는 외부 슬라이스를 위한 문인데 내부에서 그 문을 통해 돌아다니니 원이 생긴다는 것. 그래서 규칙이 나옵니다 — 안에서는 직접 경로, 밖에서는 public API.
황대웅 님은 아예 그 질문 하나로 글 전체를 세웁니다. R이 범용 언어로 만들어지지 않았다는 사실에서 왜 남았는지가 나옵니다.
셋 다 "이 도구가 왜 이렇게 생겼는지"를 알고 나서 사용 규칙을 정합니다. 그 순서를 건너뛰면 도구를 탓하게 되고, 거치면 어디까지 쓸 수 있는지가 보입니다.
편의 장치가 비용이 되는 자리
두 번째 공통점은 좋아 보이는 것에 값이 붙어 있다는 관찰입니다.
lazy var는 "필요할 때 만들자"는 편의 문법인데, 초기화 시점에 self를 바꾸기 때문에 값 타입의 non-mutating 함수에서는 못 씁니다. 읽기처럼 보이는 접근이 사실은 쓰기였던 거예요.
index도 마찬가지입니다. 내부 구조를 감추려고 만든 문인데, 크게 만들면 tree shaking이 안 되고, 많이 만들면 HMR이 느려지고, 안에서 쓰면 순환참조가 납니다.
SWXMLHash의 subscript 방식도 그렇습니다. xml["user"]["items"][0] — 직관적이라서 골랐다가 하드코딩 문자열이 코드 곳곳에 박힙니다.
대안을 검토하고 기각 이유를 남겼다
세 번째는 기록의 형태입니다. 셋 다 고르지 않은 쪽을 적었어요.
주한솔 님은 세 가지 방식 중 둘을 왜 버렸는지 적었고, 이승민 님은 헤더를 shared에 두면 왜 안 되는지를 코드로 대비시켰습니다. 황대웅 님은 Python이 강한 영역을 먼저 인정하고 나서 R의 자리를 말했고요.
기각 이유가 남아 있으면 나중에 조건이 바뀌었을 때 다시 판단할 수 있습니다. 결론만 남은 기록은 그게 안 됩니다.
이번 회차에서 가져갈 것
1. API를 만들 땐 쓰는 모습을 먼저 그리기.
내부 구조부터 잡으면 쓰기 어려운 인터페이스가 나옵니다. 최종 사용 형태를 정하고 역으로 설계하면 JSONDecoder처럼 익숙한 모양에 맞출 수 있습니다.
2. struct에서 stack.last?.something.append(...)는 반영되지 않는다.
값 타입은 복사본을 돌려주니 인덱스로 직접 접근해야 합니다. 컬렉션 원소를 제자리에서 고칠 때 걸리는 자리입니다.
3. lazy var의 getter는 mutating이다.
struct의 non-mutating 함수 안에서 쓰면 컴파일이 안 됩니다. 생성 비용이 작다면 lazy를 빼는 게 답일 때가 많습니다.
4. public API는 바깥을 향한 문으로만 쓰기.
같은 슬라이스 안에서는 직접 경로로, 다른 슬라이스는 index로. 이 구분 하나가 순환참조 대부분을 막습니다.
5. 관련성 낮은 것들을 한 index에 몰지 않기.
shared/ui 전체를 한 문으로 내보내면 버튼 하나 쓰려다 캐러셀까지 딸려 옵니다. 컴포넌트 단위로 작은 index를 두고 @/shared/ui/button처럼 직접 가져오세요.
6. "재사용되니까 shared"가 아니라 "무엇을 아는가"로 자리를 정하기.
로고와 링크만 있으면 shared, 로그인 상태를 알면 widgets입니다. 도메인이나 로직을 아는 순간 레이어가 올라갑니다.
7. 인기와 적합은 다르다.
범용 언어가 아니어서 살아남은 사례가 있습니다. 도구를 고를 때 점유율만 보면 그 도구가 원래 무엇을 잘하려고 만들어졌는지를 놓칩니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.
좋은 글 남겨주신 주한솔 님, 이승민 님, 황대웅 님께 감사드립니다! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.