안녕하세요! 그로스로그 입니다!😊 🌱
import 한 줄이 표준이 되기까지 15년이 걸린 이야기, 그리고 디자인만 하던 사람이 직접 앱을 만들어본 기록. 3기 9회차 첫 번째입니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.
이번 회차는 큐레이션이 네 편이라 두 번에 나눠 소개합니다. 먼저 선택지가 여럿인 이유에 대한 두 편이에요.
01
📦 자바스크립트 모듈 시스템
이종경 님
지금 우리가 쓰는
import/export가 어떻게 표준이 됐는지를 역사 순서로 따라간 글입니다.<script> 태그만 있던 시절 → CommonJS(서버) → AMD(브라우저) → UMD(양쪽 호환) → ESM(공식 표준)까지요.각 단계마다 어떤 문제 때문에 나왔고 어떤 한계가 있었는지를 짚습니다. 마지막엔 네 방식을 표로 비교해요.
require와 import가 왜 둘 다 존재하는지 궁금했다면, 이 글이 답입니다.
조금 더 들어가 보면
시작점은 모듈이 아예 없던 시절입니다.
// A.js
var name = 'foo';
// B.js
function sayHello() {
alert('Hello ' + name); // Hello foo
}
<script src="/src/A.js" />
<script src="/src/B.js" />
B.js가 A.js의 name을 그냥 씁니다. 아무것도 가져오지 않았는데 접근이 돼요. 모든 파일이 하나의 전역 스코프를 공유했기 때문입니다.
편해 보이지만 문제가 심각합니다. 글이 짚은 대로 "변수 충돌이 자주 발생하거나 로드 순서에 따라 의존성이 깨지는 문제"가 나요. 라이브러리 두 개가 같은 이름을 쓰면 나중에 로드된 게 앞의 것을 덮어버립니다. <script> 태그 순서 하나 바꿨다가 사이트가 통째로 깨지는 일이 흔했죠.
🔀 그래서 두 갈래로 갈렸다
여기서 흥미로운 지점이 나옵니다. 환경이 다르니 해법도 갈렸어요.
CommonJS는 서버(Node.js)를 위해 만들어졌습니다. 특징은 동기 로딩이에요.
const foo = require('./foo');
require()를 만나면 코드 실행을 멈추고 파일을 읽어옵니다. 서버에서는 이게 문제가 안 돼요. 파일이 같은 디스크에 있으니 금방 읽히거든요.
그런데 브라우저에서는 이게 치명적입니다. 파일을 네트워크로 받아야 하는데, 그동안 화면이 멈춰 있으면 안 되니까요. 글의 표현대로 "브라우저 환경에서는 CommonJS 모듈 시스템을 직접 사용할 수 없다는 근본적인 한계"입니다.
그래서 브라우저 쪽에서 AMD가 나옵니다. 비동기 로딩이에요.
define(['./util', 'jquery'], function (util, $) {
// 의존 모듈을 모두 불러온 뒤 이 함수 실행
function showValue(x) {
$('#result').text(util.compute(x));
}
return { showValue };
});
"필요한 것들을 먼저 배열로 적어두고, 다 받아지면 그때 실행하라"는 구조입니다. 받는 동안 브라우저는 다른 일을 할 수 있죠.
글이 짚은 배경이 재미있습니다. AMD는 "본래 CommonJS 진영에서 브라우저에서도 활용 가능한 방식을 논의했으나 합의에 이르지 못해 별도의 그룹으로 독립"해서 만들어졌다고요. 기술적 이견이 커뮤니티 분열로 이어진 사례입니다.
🌉 UMD — 임시 봉합
두 진영이 갈리자 라이브러리 만드는 사람들이 곤란해집니다. 어느 쪽에 맞춰야 할까요?
그래서 나온 게 UMD입니다. 코드가 그 사정을 그대로 보여줍니다.
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
define(['lodash'], factory); // AMD 환경
} else if (typeof exports === 'object' && typeof module !== 'undefined') {
module.exports = factory(require('lodash')); // CommonJS 환경
} else {
root.MyLibrary = factory(root._); // 브라우저 전역
}
}(typeof globalThis !== 'undefined' ? globalThis : this, function (_) {
function doSomething() { /* ... */ }
return { doSomething };
}));
실행 중에 "여기가 어디인지" 물어보고 분기합니다. define이 있으면 AMD, module.exports가 있으면 CommonJS, 둘 다 없으면 전역 변수.
정직한 해법이지만 모듈 본체보다 껍데기가 더 복잡합니다. 표준이 없을 때 어떤 비용을 치르게 되는지 보여주는 코드예요.
✅ ESM — 언어가 직접 나서다
2015년 ES6에서 마침내 언어 차원의 표준이 생깁니다.
// math.mjs
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
export default add;
// main.mjs
import myAdd, { multiply as mul } from './math.mjs';
깔끔한 것도 있지만, 글이 짚은 두 가지 이점이 더 중요합니다.
"실제 객체나 함수의 바인딩을 직접 참조하기 때문에 순환 참조 문제도 쉽게 관리할 수 있습니다."
CommonJS는 require 시점의 값을 복사합니다. ESM은 연결을 유지해요. 그래서 나중에 값이 바뀌면 그게 반영됩니다.
"정적인 모듈 구조 덕분에 트리 쉐이킹을 통한 최적화도 용이해졌습니다."
이게 실무에서 큰 차이를 만듭니다. import는 파일 맨 위에만 올 수 있고 조건문 안에 넣을 수 없어요. 제약처럼 보이지만 덕분에 빌드 도구가 실행 전에 의존 관계를 전부 파악할 수 있습니다. 그래서 안 쓰는 코드를 잘라낼 수 있죠(트리 쉐이킹).
제약이 최적화를 가능하게 만든 경우입니다. require는 어디든 쓸 수 있어 자유롭지만, 그래서 무엇이 필요한지 미리 알 수 없어요.
큐레이터 노트
"왜 이렇게 됐는가"를 역사로 풀어낸 글이라 뽑았습니다.
require와 import가 둘 다 존재하는 건 누가 잘못해서가 아닙니다. 서버와 브라우저라는 다른 환경이 각자의 답을 냈고, 나중에 하나로 모인 결과예요. 이 배경을 알면 지금도 남아 있는 호환성 문제들이 이해됩니다.
단일 스크립트 → 이원화 → 임시 통합 → 공식 통합이라는 정리도 깔끔합니다. 네 단어로 15년을 요약했어요.
참고 자료 중에 "JavaScript 번들러로 본 조선시대 붕당의 이해"라는 글이 있더라고요. 분열과 통합의 역사라는 관점이 거기서 온 듯합니다. 여러 각도의 자료를 읽고 자기 서사를 만든 정리예요.
02
🦋 첫 Flutter 도전기
황자혜 님
"디자인만 하던 내가, Flutter로 직접 앱을 만들어보기로 했다."
감정과 방 상태를 기록하면 맞춤 미션을 추천해주는 앱을 만드는 과정입니다. Flutter를 고른 이유부터 시작해서, 막혔던 지점 세 가지, 얻은 팁, 그리고 장점과 단점을 솔직하게 적었어요.
특히 디자이너의 눈으로 본 Flutter라는 점이 이 글을 특별하게 만듭니다.
앞선 글이 "왜 여러 방식이 생겼나"의 역사라면, 이 글은 지금 갈라진 선택지 앞에서 고르는 이야기입니다.
조금 더 들어가 보면
먼저 선택 과정이 명확합니다. 네이티브 / React Native / Flutter 세 가지를 놓고 여덟 항목으로 비교했어요. 개발 언어, UI 렌더링, 퍼포먼스, 디자인 자유도, 생태계, 웹 지원, 난이도, 그리고 디자이너 친화도.
마지막 항목이 이 글의 관점을 보여줍니다. "Figma → Widget 전환이 쉽다"는 기준으로 도구를 평가한 거예요.
그리고 결론이 이렇습니다.
"앱 같은 앱"이라는 표현이 정확합니다. Flutter는 Skia라는 자체 렌더링 엔진으로 화면을 직접 그립니다. 네이티브 컴포넌트를 빌려 쓰는 게 아니라요. 그래서 디자인한 대로 나옵니다. 디자이너에게는 이게 결정적인 차이예요.
🎨 아는 것에 빗대어 익히기
이 글에서 가장 인상적인 부분은 새 개념을 자기가 아는 것으로 번역한 방식입니다.
Row, Column, Expanded, SizedBox → 마치 Figma의 Auto Layout처럼 느껴졌다.정확한 대응입니다. Figma의 Auto Layout도 가로/세로 방향을 정하고, 요소가 남는 공간을 어떻게 나눠 가질지를 정하죠. Flutter의 Row/Column/Expanded가 하는 일이 그겁니다.
Scaffold, AppBar, FloatingActionButton → 템플릿을 이해하면 재구성은 오히려 빠르다.이것도 좋은 관찰이에요. Scaffold는 화면의 기본 뼈대입니다. 상단 바, 본문, 하단 바, 떠 있는 버튼의 자리가 미리 정해져 있어요. 디자인 시스템의 템플릿과 같은 개념이죠.
그리고 상태 관리를 설명한 대목이 특히 좋았습니다.
StatefulWidget은 내부에 상태(State)를 따로 관리하는 구조로, React의 useState()와 유사한 역할을 한다. 하지만 React는 함수 안에서 상태를 직접 선언하고, Flutter는 상태 클래스를 분리해서 관리한다는 점이 다르다."비슷한 점과 다른 점을 둘 다 짚었어요. 새 기술을 배울 때 "이건 저것과 같다"로만 이해하면 나중에 다른 지점에서 걸립니다. 어디까지 같고 어디부터 다른지를 아는 게 정확한 이해예요.
🧭 막혔던 지점들
세 가지를 적어뒀는데, 각각 다른 종류의 문제입니다.
1. 로그인 후에도 회원가입이 실행됨 — context.push() 뒤에 return을 안 써서 코드가 계속 흘렀던 문제입니다. 화면을 옮기는 함수가 그 아래 코드를 멈춰주지는 않는다는 걸 모르면 딱 걸리는 지점이에요.
2. SafeArea 블랙박스 깜빡임 — 배경색을 안 준 화면에서 노치와 하단 영역이 깜빡였습니다. 기기의 물리적 형태 때문에 생기는 문제라 웹만 하던 사람에겐 낯설죠.
3. Supabase 403 Unauthorized — 사진 업로드가 막혔습니다. 원인은 RLS(Row Level Security) 정책이었어요.
세 번째에 대한 팁이 특히 값집니다.
CREATE POLICY "User uploads to own folder only"
ON storage.objects
FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'user_mission_photos'
AND storage.foldername(name) = auth.uid()::text
);
"자기 폴더에만 올릴 수 있다"를 DB 정책으로 표현한 겁니다. 애플리케이션 코드가 아니라 데이터베이스가 직접 막아요. 코드에 버그가 있어도 남의 폴더는 못 건드립니다. 좋은 보안 설계예요.
🧭 단점을 정직하게 적은 것
이 글이 신뢰를 얻는 부분입니다.
정확한 지적입니다. 웹 개발자는 F12를 열고 요소를 클릭해 값을 바꿔가며 화면을 맞춥니다. 그 즉각적인 반응이 없으면 답답하죠. Flutter Inspector가 있지만 결이 다릅니다.
flutter clean 하고 다시 빌딩 해야 적용되는 업데이트들도 있어서 딜레이가 많아 살짝 😫.."Hot Reload가 빠르다고들 하지만 안 먹히는 경우도 있다는 현실을 적었어요. 좋은 점만 적은 글보다 훨씬 쓸모 있습니다.
큐레이터 노트
경계를 넘어간 기록이라 뽑았습니다.
디자이너가 앱을 직접 만들어보는 건 쉬운 결정이 아닙니다. 익숙한 도구를 놓고 낯선 곳에서 처음부터 시작해야 하니까요. 그런데 이 글은 그 과정에서 자기가 가진 것을 버리지 않았어요. Auto Layout으로, 모듈 단위 디자인으로, Figma의 감각으로 새 개념을 번역했습니다.
그 관점이 오히려 강점이 됩니다. 개발자가 쓴 Flutter 입문 글은 많지만, "디자이너 친화도"를 비교 항목에 넣은 글은 드물어요. 같은 자리에 있는 사람에게 정확히 필요한 정보입니다.
그리고 마지막 문장이 마음에 남았습니다.
"프로토타입을 진짜 앱으로" — 디자이너가 개발을 배우는 이유가 이 한 줄에 있습니다. 그리고 안드로이드 기기가 없어서 .apk로 뽑아 폰에 넣어봤다는 부분도요. 어떻게든 실제로 돌려본 것이 이 도전을 완성시켰습니다.
프로필 문구도 좋았어요. "I Design My Own Trajectory."
💡 두 편을 겹쳐 읽으며
한 편은 15년의 표준화 역사, 한 편은 첫 도전기입니다. 시간의 규모가 완전히 다릅니다.
그런데 두 글이 같은 질문 앞에 서 있어요. "왜 선택지가 여럿인가."
이종경 님의 글은 그 답을 역사로 보여줍니다. CommonJS와 AMD가 갈린 건 서버와 브라우저라는 환경이 달랐기 때문이에요. 각자 자기 환경의 문제를 풀었고, 그래서 둘 다 옳았습니다.
황자혜 님의 글은 그 답을 선택으로 보여줍니다. Flutter와 React Native가 둘 다 있는 건 중요하게 여기는 것이 다르기 때문이에요. 디자인 자유도가 중요하면 Flutter, JS 경험을 살리고 싶으면 React Native.
선택지가 여럿인 건 대개 문제가 여럿이기 때문입니다. 하나가 다른 하나보다 나은 게 아니라, 서로 다른 문제를 풀고 있는 거죠.
그래서 기술을 고를 때 물어야 할 건 "뭐가 더 좋아요?"가 아니라 "내가 풀려는 문제가 뭐죠?"입니다. 황자혜 님이 여덟 항목을 비교하며 실제로 한 일이 그겁니다.
그리고 언젠가는 통합되기도 합니다. ESM처럼요. 다만 그러기까지 15년이 걸렸고, 그 사이에도 사람들은 UMD 같은 걸 만들어가며 계속 만들었어요. 완벽한 표준을 기다리지 않고요.
이번 회차에서 가져갈 것
1. require와 import가 다 있는 이유를 알아두기. 서버(동기)와 브라우저(비동기)라는 다른 환경이 각자 낸 답입니다. 지금 남아 있는 호환성 문제들의 뿌리예요.
2. import의 제약이 최적화를 만든다. 파일 맨 위에만 쓸 수 있어 불편하지만, 덕분에 빌드 도구가 미리 의존 관계를 파악해 안 쓰는 코드를 잘라냅니다.
3. 새 기술은 아는 것에 빗대어 익히되, 다른 점도 짚기. "Auto Layout 같다"로 시작하되 어디부터 다른지를 확인하세요. 같은 줄로만 알면 나중에 걸립니다.
4. 화면 이동 함수는 코드를 멈추지 않는다. push() 뒤에 return을 안 쓰면 아래 코드가 계속 돕니다. "로그인했는데 회원가입도 실행되는" 종류의 버그예요.
5. 권한은 DB에서 막는 게 안전하다. Supabase RLS처럼 데이터베이스 정책으로 막아두면 애플리케이션 코드에 버그가 있어도 남의 데이터는 안전합니다.
좋은 글 남겨주신 이종경 님, 황자혜 님께 감사드립니다! 😊
9회차 두 번째 큐레이션으로 이어집니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.
'성장일지' 카테고리의 다른 글
| [GL 3기 10회차 ①] 규칙을 먼저 정하고 시작하기 — REST API 설계, 3년의 선택이 회사가 되기까지 🌱 (0) | 2026.08.07 |
|---|---|
| [GL 3기 9회차 ②] 올리면 알아서 되는 것들 — PR 미리보기 배포, CSS가 SEO에 닿는 지점 🌱 (0) | 2026.08.07 |
| [GL 3기 8회차 ①] 같은 약자 다른 뜻 — CSR 렌더링과 인증서, 그리고 타입스크립트 🌱 (0) | 2026.08.07 |
| [GL 3기 7회차 ②] 만들기 전에 고르는 것 — 앱 개발 방식 선택, ArrayList와 LinkedList 🌱 (0) | 2026.08.07 |
| [GL 3기 7회차 ①] 남의 것을 내 것으로 — 설치하지 않고 복사해 쓰는 컴포넌트, 제로 베이스 CI/CD 🌱 (0) | 2026.08.07 |