안녕하세요! 그로스로그 입니다!😊 🌱
save()를 두 번 불렀는데 INSERT는 한 번만 나가는 이유, 코딩을 배우기 전에 먼저 봐야 할 것, 그리고 개발자가 피그마를 여는 이유. 4기 1회차 세 편입니다.
4기 1회차는 2025년 9월, 기수가 막 시작하던 무렵입니다. 이번 회차에는 누군가 알아서 해주던 자리를 한 번 열어본 기록 세 편이 모였습니다. 프레임워크가 알아서 해주던 것, 학원이 알아서 가르쳐준다고 믿었던 것, 디자이너가 알아서 넘겨주던 것이요.
01
🗄️ [JPA] save()는 두 번인데, INSERT는 한 번만? - 영속성 컨텍스트
한대천 님
Spring Boot + JPA에서 같은 엔티티에
save()를 두 번 호출했는데, 로그에는 INSERT가 한 번만 찍힙니다.그 이유가 영속성 컨텍스트라는 것을 짚고,
flush()와 clear()를 사이에 넣으면 INSERT가 두 번 나간다는 것까지 실험으로 확인했어요.마지막에 "이 글로 답할 수 있는 질문" 세 개를 스스로 붙여뒀습니다.
버그처럼 보이는데 버그가 아닌 장면이 있습니다. 이 글이 붙잡은 게 딱 그런 자리예요. 같은 객체를 두 번 저장했으면 두 번 들어가는 게 상식인데, 로그에는 한 줄뿐입니다.
저자는 "거의 코드 패턴과 눈치로 써먹었던" 것들을 다시 정리하는 중이라고 적었습니다. 돌아가긴 하는데 왜 돌아가는지는 모르는 상태요. 이 회차의 다른 두 편과 나란히 놓으면 이 문장이 회차 전체의 주제가 됩니다.
조금 더 들어가 보면
🧾 영속성 컨텍스트는 트랜잭션이 열려 있는 동안의 장부입니다
JPA를 처음 배우면 Repository가 DB에 바로 쏘는 물건처럼 보입니다. 그런데 실제로는 사이에 층이 하나 더 있어요. 영속성 컨텍스트(Persistence Context) 입니다.
이 층은 트랜잭션이 열릴 때 생기고, 커밋되거나 롤백될 때 사라집니다. 그 사이에 다룬 엔티티를 키(식별자) 기준으로 들고 있어요. 이걸 1차 캐시라고 부릅니다.
역할이 넷입니다.
· 1차 캐시 — 같은 식별자로 다시 조회하면 DB에 안 가고 들고 있던 객체를 그대로 돌려줍니다
· 동일성 보장 — 그래서 같은 트랜잭션 안에서 두 번 조회한 결과는 equals()가 아니라 ==로도 같습니다
· 쓰기 지연 — persist()를 불러도 그 자리에서 INSERT가 나가지 않고 SQL을 모아둡니다
· 변경 감지(더티 체킹) — 처음 읽어온 상태를 스냅샷으로 떠두고, 커밋 직전에 지금 값과 비교해서 달라진 필드만 UPDATE를 만듭니다
setName()만 했는데 UPDATE가 나가는 게 이상하게 느껴졌다면, 그게 네 번째 역할 때문입니다. 저장하라고 시킨 적이 없는데 저장됩니다.
🔀 save()는 하나의 동작이 아닙니다
이 글의 핵심이 여기입니다. Spring Data JPA의 save()는 이름과 달리 두 갈래로 갈라집니다.
· 처음 보는 엔티티 → EntityManager.persist()
· 이미 알고 있는 엔티티 → EntityManager.merge()
"처음 보는가"의 판정은 기본적으로 식별자가 비어 있는가로 합니다. @GeneratedValue를 쓰는 엔티티라면 id가 null인 동안은 새것이고, 한 번 저장돼 값이 채워지면 그때부터는 새것이 아닙니다.
그래서 첫 번째 save()에서 엔티티가 영속성 컨텍스트에 등록되고, 두 번째 save()는 이미 장부에 올라 있는 대상을 다시 넘긴 셈이 됩니다. 새로 넣을 이유가 없으니 INSERT가 늘지 않아요.
정상 동작입니다. 다만 "정상이니까 됐다"로 끝내면 다음 두 장면에서 막힙니다.
⏳ 쓰기 지연 — INSERT는 언제 나가는가
persist()를 부른 시점에는 SQL이 DB로 안 갑니다. 영속성 컨텍스트 안의 쓰기 지연 SQL 저장소에 쌓여 있다가, 플러시(flush) 시점에 한꺼번에 나갑니다.
플러시가 일어나는 때는 셋입니다.
1. 트랜잭션이 커밋될 때 (대부분 여기)
2. JPQL·QueryDSL 같은 쿼리가 실행되기 직전 (모아둔 걸 안 보내면 조회 결과가 틀리니까요)
3. em.flush()를 직접 부를 때
2번이 은근히 중요합니다. 저장하고 나서 바로 JPQL로 조회하면 "아직 안 넣었는데 왜 나오지?" 싶은 일이 생기는데, 조회 직전에 자동으로 플러시가 돌기 때문이에요.
그리고 플러시는 커밋이 아닙니다. SQL을 DB로 보내는 것뿐이라 롤백하면 그대로 사라집니다. 이 둘을 같은 것으로 알고 있으면 트랜잭션 경계에서 헷갈립니다.
🧹 clear()가 하는 일 — 준영속으로 떨어뜨리기
원문의 두 번째 실험이 이걸 보여줍니다. flush()로 DB에 보내고 clear()로 장부를 비우면, 같은 자바 객체인데도 JPA가 모르는 물건으로 취급합니다. 이 상태를 준영속(detached)이라고 해요.
그래서 두 번째 save()가 "처음 보는 것"으로 처리되고 INSERT가 한 번 더 나갑니다. 같은 코드인데 사이에 두 줄이 들어갔다고 결과가 달라지는 것이죠.
여기서 실무 이야기 하나가 붙습니다. 대량 배치요. 10만 건을 반복문으로 save() 하면 영속성 컨텍스트에 10만 개가 쌓입니다. 1차 캐시가 계속 커지고, 커밋 직전에 10만 개를 전부 스냅샷과 비교하느라 느려지고, 심하면 메모리가 터집니다.
그래서 배치에서는 일정 개수마다 flush() + clear()를 넣어 장부를 비우는 게 정석입니다. 이 글의 실험 코드가 그대로 배치 최적화의 뼈대예요.
⚠️ merge()의 비용과 벌크 연산의 함정
두 가지만 더 붙이겠습니다. 알고 나면 사고를 한 번씩 막아주는 것들입니다.
첫째, merge()는 공짜가 아닙니다. 준영속 엔티티를 save() 하면 merge()가 불리는데, 이때 JPA는 DB에서 먼저 SELECT를 한 번 합니다. 원본을 읽어와 값을 덮어쓴 뒤 새로 영속화하거든요. 그리고 돌려주는 건 인자로 넘긴 그 객체가 아니라 새로 만든 영속 객체입니다. merge(a) 하고 a를 계속 쓰면 변경이 반영되지 않아요.
둘째, 벌크 연산은 1차 캐시를 모릅니다. @Modifying 붙인 UPDATE 쿼리는 DB에 직접 나갑니다. 그런데 영속성 컨텍스트에는 옛날 값이 그대로 남아 있어요. 그래서 벌크 연산 뒤에 같은 엔티티를 조회하면 DB와 다른 값이 나옵니다. 해법은 같습니다. 벌크 연산 뒤에 clear()를 부르는 것이요.
두 함정 모두 뿌리가 하나입니다. DB와 장부가 두 벌 존재한다는 것. 이 글이 짚은 그 지점입니다.
🚪 장부는 언제 열리고 언제 닫히는가
원리를 알고 나면 자연히 따라오는 질문이 있습니다. "그럼 이 장부는 정확히 언제부터 언제까지 살아 있는가."
답은 트랜잭션 경계입니다. @Transactional이 붙은 메서드에 들어갈 때 열리고, 나올 때 닫혀요. 그래서 같은 코드라도 어디에 @Transactional을 붙였느냐에 따라 동작이 달라집니다.
이게 실제로 사고가 나는 자리입니다. 조회한 엔티티를 트랜잭션 밖으로 들고 나가면 그 객체는 준영속이 됩니다. 값을 바꿔도 더티 체킹이 안 돌아요. "분명히 값을 바꿨는데 DB에 반영이 안 된다" 는 상황의 흔한 원인이 이겁니다.
지연 로딩도 같은 이유로 터집니다. JPA는 연관된 엔티티를 당장 안 읽고 프록시라는 껍데기만 넣어둡니다. 실제로 값을 꺼내는 순간 그때 조회하는데, 그 순간에 트랜잭션이 이미 닫혀 있으면 조회할 창구가 없어 예외가 납니다.
스프링 부트가 기본으로 켜두는 OSIV(Open Session In View)라는 설정이 이 문제를 뒤로 미뤄줍니다. 응답이 나갈 때까지 영속성 컨텍스트를 살려두거든요. 편한데 대가가 있습니다. 요청 하나가 끝날 때까지 DB 커넥션을 붙잡고 있어서, 트래픽이 몰리면 커넥션 풀이 먼저 마릅니다.
그래서 규모가 있는 서비스는 OSIV를 끄고, 필요한 데이터를 트랜잭션 안에서 다 꺼내 DTO로 바꿔 내보내는 쪽을 택합니다. 어느 쪽이든, 장부가 언제 닫히는지 모르면 고를 수 없는 선택입니다.
🔍 N+1 — 가장 자주 만나는 청구서
영속성 컨텍스트를 이야기했으니 그 옆에 붙어 있는 문제도 짚고 가겠습니다. JPA를 쓰면 거의 반드시 한 번은 만나는 자리예요.
주문 목록 100건을 조회하고, 각 주문의 회원 이름을 화면에 뿌린다고 해봅시다. 코드는 이렇게 생깁니다.
List<Order> orders = orderRepository.findAll(); // 쿼리 1번
for (Order o : orders) {
System.out.println(o.getMember().getName()); // 여기서 100번
}
보기에는 반복문 하나인데 SQL은 101번 나갑니다. 목록을 가져오는 1번, 그리고 회원을 꺼낼 때마다 1번씩 100번이요. 이걸 N+1 문제라고 부릅니다.
원인이 앞에서 이야기한 지연 로딩입니다. 연관된 회원을 당장 안 읽고 프록시만 넣어뒀다가, getName()을 부르는 순간 그제야 조회하거든요. 한 건일 때는 합리적인 동작인데, 목록에서 돌면 곱해집니다.
무서운 건 개발할 때는 안 보인다는 점입니다. 테스트 데이터가 10건이면 11번이라 체감이 없어요. 그런데 운영에서 1,000건이 되면 1,001번이 됩니다.
해법은 필요한 걸 미리 같이 읽어오는 것입니다. 조회할 때 조인해서 한 번에 가져오면 쿼리가 1번으로 끝나요. 목록이 여러 갈래로 갈라지는 경우에는 한 번에 다 조인하지 말고, 연관마다 따로 모아 읽는 방식이 낫습니다. 그러면 101번이 2~3번이 됩니다.
그리고 이 문제를 발견하려면 SQL이 몇 번 나갔는지 봐야 합니다. 로그에 쿼리를 찍어두는 설정을 켜두는 게 그래서 기본이에요. 이 글이 로그를 보고 시작한 것처럼요.
🧭 이 글이 좋은 지점 — 로그부터 봤습니다
돌아보면 이 기록의 출발점이 로그입니다. save()를 두 번 불렀는데 로그에 INSERT가 한 줄뿐이라는 관찰에서 시작해요.
JPA를 쓸 때 가장 먼저 켜야 하는 게 이겁니다. 내가 쓴 자바 코드와 실제로 나가는 SQL 사이에 층이 하나 있으니까요. 그 층이 무엇을 만들어내는지 안 보면, 코드는 멀쩡한데 쿼리가 이상한 상황을 알아챌 방법이 없습니다.
방금 이야기한 N+1도, 벌크 연산 뒤의 불일치도, 더티 체킹으로 나가는 UPDATE도 전부 로그에서 먼저 보입니다. 그래서 이 글이 붙잡은 장면이 사실은 좋은 습관의 결과예요. 로그를 보고 있었으니 이상함을 느낀 것이고, 이상함을 느꼈으니 원리까지 내려간 것입니다.
📝 원문에서는 이렇게 확인합니다
같은 코드에 두 줄을 넣어 결과를 갈라 보였습니다.
@Transactional
public void saveTwiceWithFlush() {
Member member = new Member("CHEETAH");
memberRepository.save(member);
em.flush(); // DB 반영
em.clear(); // 영속성 컨텍스트 초기화
memberRepository.save(member); // 새 엔티티처럼 처리됨
}
이 두 줄이 없으면 INSERT가 한 번, 있으면 두 번입니다. 말로 설명하는 대신 조건을 하나만 바꿔서 보여주는 방식이라 읽는 사람이 직접 재현할 수 있어요.
큐레이터 노트
정상 동작인데 이상해 보이는 장면을 그냥 넘기지 않고 원리까지 내려간 기록입니다. "왜 그런지까지 갔다"는 선정 신호에 정확히 해당해요. 특히 좋았던 건 조건을 하나만 바꿔 결과를 갈라 보인 실험입니다. 설명이 아니라 재현 가능한 근거라서요.
마무리에 붙인 "이 글로 답할 수 있는 질문" 세 개도 눈에 띕니다. 배운 것을 답할 수 있는 형태로 바꿔두는 습관인데, 이게 쌓이면 그대로 자기 지식의 목차가 됩니다.
02
🧭 [CS 공부 #1] 코딩 학원 등록 전에, 진짜 알아야 할 것
홍범영 님
컴퓨터과학을 10편에 걸쳐 다루겠다는 연재의 첫 글입니다. 이 편에는 코드가 한 줄도 없어요.
문제 해결을 '찾는 힘'과 '푸는 힘' 으로 나누고, 컴퓨터과학을 "세상에 늘 있던 문제를 디지털로 옮겨 푸는 학문"으로 정의합니다.
그리고 대학 커리큘럼과 반대 순서로 가겠다고 선언합니다. 네트워크부터 시작해 아래로 내려가겠다고요.
첫 편에 코드가 없는 글을 큐레이션에 올리는 건 이번이 처음이 아닙니다. 1기 2회차도 두 편 다 코드가 0줄이었어요. 판단 기준은 길이나 코드 유무가 아니라 남는 게 있는가입니다.
이 글은 열 편짜리 목차를 먼저 공개하고 시작합니다. 끝을 정해놓고 출발한 기록이라서, 뒤에 이어지는 회차들과 겹쳐 읽을 때 값이 커집니다.
조금 더 들어가 보면
🔍 문제를 '찾는 힘'과 '푸는 힘'은 실제로 다른 능력입니다
글의 뼈대가 이 구분입니다. 그리고 이건 소프트웨어 공학에서 오래된 주제예요.
프레드 브룩스가 1986년 「No Silver Bullet」에서 소프트웨어의 어려움을 본질적 복잡성과 부수적 복잡성으로 나눴습니다. 언어와 도구가 좋아지면서 부수적인 쪽은 계속 줄었는데, 본질적인 쪽 — 무엇을 만들 것인지 정하는 일 — 은 거의 안 줄었습니다.
그가 남긴 문장이 이 글의 주장과 같은 자리를 가리킵니다.
요구사항 단계에서 잘못 잡은 방향은 코딩을 아무리 잘해도 되돌릴 수 없습니다. 결함을 늦게 발견할수록 고치는 비용이 몇 배씩 커진다는 것도 오래전부터 반복 확인된 사실이고요.
AI가 코드를 대신 쓰기 시작한 지금은 이 구분이 더 선명해졌습니다. 부수적 복잡성 쪽은 빠르게 자동화되는데, "무엇이 문제인가"를 정하는 자리는 그대로 남아 있어요. 이 글이 코딩 학원 이야기로 문을 연 게 우연이 아닙니다.
🏛️ 컴퓨터과학은 없던 것을 만든 게 아니라는 말
저자가 든 세 가지 대응이 정확합니다. 장부 → 데이터베이스, 도서관 사서의 정리 노하우 → 자료구조, 요리 레시피 → 알고리즘.
비유로 넘길 문장이 아니라 실제로 그렇습니다.
· 알고리즘이라는 단어 자체가 9세기 수학자 알콰리즈미의 이름에서 왔습니다. 계산 절차를 규칙으로 적는 일이 컴퓨터보다 천 년 먼저 있었어요
· 데이터베이스의 인덱스는 도서관 카드목록과 구조가 같습니다. 원본을 그대로 두고 찾는 길만 따로 만들어두는 것이요
· 회계의 복식부기는 트랜잭션의 원형입니다. 한 사건을 두 군데 동시에 적고, 둘이 안 맞으면 잘못된 것으로 보는 규칙이요
그래서 컴퓨터과학이 하는 일은 '형식화'라고 부르는 게 정확합니다. 사람이 감으로 하던 절차를 기계가 실행할 수 있을 만큼 애매함 없이 적는 것. 이 글의 (정의) → (분석) → (절차) 3단이 그 과정 그대로입니다.
🔄 커리큘럼을 뒤집은 판단
이 글에서 가장 큰 결정은 순서입니다. 보통의 전공 커리큘럼은 아래에서 위로 쌓습니다. 논리회로 → 컴퓨터구조 → 운영체제 → 네트워크 순이요. 이 연재는 반대로 갑니다. 네트워크에서 시작해 아래로 내려갑니다.
교육학에서는 이걸 나선형 접근이라고 부릅니다. 제롬 브루너가 정리한 방식인데, 학습자에게 이미 친숙한 것에서 출발해 같은 주제를 점점 깊이 다시 만나게 하는 구조예요. 와이파이가 안 되는 경험은 누구나 있으니 네트워크는 시작점으로 좋습니다.
다만 이 순서에는 대가가 있습니다. 추상화 누수요. 위층만 알고 있으면 평소에는 잘 돌아가다가, 뭔가 새면 아래층 지식이 없어 막힙니다. 지연 시간이 왜 이만큼인지, 왜 이 요청만 느린지 같은 질문은 결국 아래층으로 내려가야 답이 나와요.
그래서 이 순서는 "아래로 내려간다"는 약속이 지켜질 때만 성립합니다. 저자가 목차 열 개를 미리 공개한 게 그 약속을 스스로에게 건 장치로 보입니다.
🧰 코드를 안 써도 남는 세 가지
저자가 꼽은 세 가지 — 시스템적 사고, 공통의 언어, 세상을 보는 관점 — 는 2006년 자넷 윙이 정리한 계산적 사고(Computational Thinking) 와 거의 같습니다. 윙은 이걸 네 가지로 나눴어요.
| 요소 | 하는 일 | 이 글의 예시 |
|---|---|---|
| 분해 | 큰 문제를 다룰 수 있는 조각으로 나눈다 | 10개 업무를 처리 시간·이동 거리로 쪼갠다 |
| 패턴 인식 | 반복되는 구조를 찾는다 | 파일 정리 문제가 자료구조 문제와 같다 |
| 추상화 | 지금 안 중요한 것을 걷어낸다 | 업무를 '측정 가능한 숫자'로 바꾼다 |
| 알고리즘 | 순서 있는 규칙으로 적는다 | 컴퓨터가 수행할 명확한 단계로 만든다 |
윙의 논문에서 가장 많이 인용되는 문장이 이 글의 결론과 같습니다. 계산적 사고는 컴퓨터과학자만의 것이 아니라 모든 사람의 기본 소양이라는 것이요.
⏱️ 늦게 발견할수록 비싸집니다
"문제를 먼저 정의하라"는 말이 원칙론처럼 들릴 수 있는데, 이건 비용 이야기이기도 합니다.
소프트웨어 공학에서 오래 반복 확인된 사실이 하나 있습니다. 결함을 늦게 발견할수록 고치는 비용이 급격히 커진다는 것이요. 요구사항 단계에서 잡으면 문장 한 줄 고치면 됩니다. 설계 단계면 구조를 다시 그려야 하고, 구현 단계면 코드를 다시 써야 하고, 배포 뒤면 이미 쌓인 데이터까지 손봐야 합니다.
단계가 하나씩 넘어갈 때마다 앞 단계의 결정 위에 새 작업이 얹히기 때문입니다. 아래를 고치려면 위에 쌓은 것을 걷어내야 하고, 걷어낼 것이 많을수록 비쌉니다.
그래서 "코딩 학원 등록 전에"라는 제목이 실용적입니다. 배우는 순서 이야기이면서, 동시에 가장 싼 자리에서 가장 중요한 결정을 하라는 이야기예요.
🪜 이 연재가 실제로 이어진 자리
첫 편의 약속이 지켜졌는지는 뒤 회차에서 확인됩니다. 예고한 목차대로 2회차에 네트워크, 3회차에 운영체제, 4회차에 알고리즘이 이어졌어요.
목차를 미리 거는 방식에는 분명한 이점이 있습니다. 다음에 뭘 쓸지 고민하는 시간이 사라진다는 것이요. 그리고 읽는 사람 입장에서도 한 편을 읽고 나면 다음 편을 기다릴 이유가 생깁니다.
기록을 이어가는 일에서 가장 흔한 실패가 "다음에 뭘 쓰지"에서 멈추는 것인데, 이 글은 그 지점을 첫 편에 미리 없앴습니다.
🎒 비전공자가 CS를 배울 때 실제로 걸리는 것
이 연재가 겨냥한 독자를 생각하면 한 가지를 덧붙일 만합니다. 코드를 안 쓰는 사람이 CS를 공부할 때 실제로 막히는 지점이요.
대개는 개념이 어려워서가 아닙니다. 왜 지금 이걸 배우는지 모르겠어서 멈춥니다.
전공 커리큘럼은 아래에서 위로 쌓기 때문에, 처음 몇 달은 "이게 나중에 어디 쓰이는지" 안 보이는 상태가 이어집니다. 논리회로를 배우는 동안 내가 만들고 싶은 것과의 거리가 너무 멀어요. 그 구간에서 대부분 그만둡니다.
이 연재의 순서가 그 문제를 정면으로 겨냥합니다. 와이파이가 안 되는 경험, 컴퓨터가 느려지는 경험처럼 이미 겪은 것에서 출발하니까요. 첫 편부터 "아 이게 그거였구나"가 나옵니다.
물론 앞에서 짚은 대로 위층만 알면 새는 지점에서 막힙니다. 그래서 이 방식은 끝까지 내려간다는 전제에서만 성립해요. 목차를 미리 걸어둔 게 그 전제를 스스로에게 건 장치이고요.
🔤 용어를 번역하지 않고 두는 것도 방법입니다
이 글에서 눈에 띄는 또 하나가 용어를 최소한만 쓴다는 점입니다. 컴퓨터과학 이야기를 하는데 전문 용어가 거의 안 나와요. 대신 회의실 배정, 파일 정리 같은 일상의 예로 갑니다.
이게 입문 글에서 쉬운 선택이 아닙니다. 용어를 쓰면 정확해지고 짧아지거든요. 안 쓰면 길어지고 느슨해집니다.
그런데 읽는 사람이 아직 그 용어를 모르면, 정확한 문장은 아무것도 전달하지 못합니다. 그래서 첫 편에서는 느슨하더라도 아는 말로 가고, 뒤에서 하나씩 이름을 붙여가는 순서가 맞아요.
저자가 마지막에 각 편의 제목을 나열해둔 걸 보면 그 계획이 보입니다. 네트워크, 운영체제, 알고리즘, 자료구조 — 편마다 하나씩 이름을 붙여갈 예정이거든요. 4회차에서 실제로 그 방식이 이어집니다.
📝 원문에서는 이렇게 씁니다
코드 대신, 감으로 하던 일을 절차로 바꾸는 3단을 보여줍니다.
(분석) 가능한 모든 경로의 수를 따져보고, 최적의 경로를 찾기 위한 규칙을 설계합니다.
(절차) 이 규칙을 컴퓨터가 수행할 수 있는 명확한 단계(알고리즘)로 만듭니다.
읽고 나면 눈치채실 텐데, 이건 외판원 문제입니다. 조합 최적화에서 가장 유명한 난제 중 하나예요. 일상의 "오늘 일 순서 어떻게 잡지"가 그대로 그 문제라는 걸 보여준 겁니다.
큐레이터 노트
열 편을 미리 목차로 걸어두고 출발한 연재의 첫 글입니다. 「시리즈로 이어진다」는 선정 신호가 가장 선명하게 붙는 자리예요. 한 편은 짧아도 모이면 지도가 되니까요.
무엇보다 커리큘럼과 반대 순서로 가겠다는 판단과 그 이유가 함께 적혀 있는 게 좋았습니다. 그냥 "이렇게 공부하겠다"가 아니라 "보통은 이렇게 하는데 나는 저렇게 하겠고 이유는 이것"이라서요. 4회차에 이 연재의 알고리즘 편이 다시 나옵니다.
03
🎨 개발자도 피그마(Figma)를 배우면 좋은 이유와 기초 가이드
주시원 님
개발자가 디자인 툴을 배우면 좋은 이유 다섯 가지를 정리하고, 익힐 기능을 꼭 알아야 할 것 셋 / 있으면 좋은 것 셋으로 나눴습니다.
기준이 분명합니다. "디자이너처럼 깊게 배울 필요는 없다" 는 전제를 먼저 깔아요.
옛날에 오가던 질문 하나를 그대로 인용합니다. "이 여백 몇 픽셀이에요?"
이 글의 값어치는 목록 자체보다 범위를 그은 데 있습니다. 새 도구를 배우라는 글은 흔한데, 어디까지만 배우면 되는지를 정해주는 글은 드물어요.
조금 더 들어가 보면
📐 "이 여백 몇 픽셀이에요?"가 사라진 자리
이 한 문장에 십 년치 변화가 들어 있습니다.
예전에는 디자이너가 PSD나 PNG를 넘기고 개발자가 그걸 보고 옮겼습니다. 파일 안의 수치를 볼 방법이 마땅치 않으니 물어볼 수밖에 없었어요. 그래서 시안에서 수치를 뽑아 문서로 만들어주는 별도 도구가 따로 있었을 정도입니다.
지금은 그 단계가 도구 안으로 들어왔습니다. 개발자가 파일을 직접 열고 요소를 클릭하면 크기·간격·색상·폰트가 그 자리에서 나옵니다. 중간에 사람이 옮겨 적는 단계가 사라진 것이죠. 옮겨 적는 단계가 없어지면 옮겨 적다 틀리는 일도 없어집니다.
⚠️ Inspect가 주는 것과, 그대로 쓰면 안 되는 것
여기서 한 발 더 들어가 보겠습니다. 저자가 "바로 코드로 옮길 수 있는 자료"라고 쓴 부분인데, 실무에서는 그대로 복사하면 대개 문제가 생깁니다.
이유가 셋입니다.
· 좌표가 절대값으로 나옵니다. 디자인 파일의 요소는 캔버스 위 특정 위치에 놓여 있으니 position: absolute에 가까운 값이 나오는데, 실제 화면은 반응형이라 그대로 쓰면 창 크기가 바뀌는 순간 무너집니다
· 값이 날것으로 나옵니다. #00962B처럼 색상 코드가 그대로 나오는데, 코드에서는 보통 --color-brand 같은 이름을 씁니다. 이름 대신 숫자를 심으면 나중에 브랜드 색을 한 번 바꿀 때 전부 찾아다녀야 해요
· 폰트가 다릅니다. 디자인 파일의 렌더링과 브라우저의 렌더링은 자간·행간 처리가 달라서, 같은 수치를 넣어도 눈에 보이는 결과가 어긋납니다
그래서 Inspect는 '정답 코드'가 아니라 '치수 도면'으로 보는 게 맞습니다. 이 구분을 알고 여는 것과 모르고 여는 것의 차이가 큽니다.
🧱 오토 레이아웃이 곧 Flexbox입니다
개발자가 피그마를 배울 때 실익이 가장 큰 지점은 사실 따로 있습니다. 오토 레이아웃(Auto Layout) 이요.
이 기능의 개념이 CSS Flexbox와 거의 1:1로 대응합니다.
| 피그마 | CSS |
|---|---|
| 세로/가로 방향 | flex-direction: column / row |
| 요소 사이 간격 | gap |
| 안쪽 여백 | padding |
| 정렬 기준점 | justify-content / align-items |
| 컨테이너에 맞춰 늘리기 | flex-grow: 1 |
| 내용에 맞춰 줄이기 | width: fit-content |
이걸 알고 나면 디자인 파일을 볼 때 "이 화면은 어떤 구조로 짜여 있는가" 가 보입니다. 화면을 그림으로 보는 게 아니라 레이아웃 트리로 보게 되는 것이죠.
반대 방향으로도 이득이 있습니다. 오토 레이아웃 없이 요소를 좌표로 흩뿌려 놓은 시안은 구현했을 때 반응형에서 깨질 가능성이 높다는 신호입니다. 시안을 받은 시점에 미리 이야기할 수 있어요.
🔁 컴포넌트와 베리언트는 props입니다
저자가 "있으면 좋은 기능"으로 꼽은 컴포넌트도 같은 맥락입니다.
피그마의 컴포넌트는 한 번 만들어두고 여기저기 갖다 쓰는 원본이고, 베리언트는 그 컴포넌트의 상태 묶음입니다. 버튼 하나에 크기(large/medium/small)와 상태(default/hover/disabled)를 붙여두는 식이요.
이게 코드의 컴포넌트 props와 구조가 같습니다. <Button size="large" state="disabled"> 하고 정확히 대응해요.
그래서 디자인 시스템이 잡혀 있는 팀에서는 이 대응이 그대로 공통 언어가 됩니다. "저 버튼 좀 크게 해주세요" 대신 "size를 large로"라고 말하게 되고, 양쪽이 같은 이름을 쓰게 되니까요. 이름을 맞추는 것만으로 오해가 줄어듭니다.
🔀 프로토타입은 화면 목록이 아니라 상태 전이입니다
마지막으로 프로토타입 이야기입니다. 저자는 "사용자 흐름을 미리 체험할 수 있다"고 적었는데, 개발자 쪽에서 보면 이건 조금 다르게 읽힙니다.
프로토타입에서 화면과 화면을 잇는 화살표는 상태 전이입니다. 어떤 조건에서 어디로 가는가요. 이걸 미리 보면 구현하기 전에 세 가지를 알 수 있습니다.
· 몇 개의 상태가 있는가 — 화면 수가 아니라 상태 수요. 같은 화면이 로딩·성공·빈 목록·오류로 네 가지일 수 있습니다
· 어떤 전이가 빠져 있는가 — 시안에 오류 화면이 없으면 그건 아직 정해지지 않은 것입니다. 구현 중에 물어보는 것보다 이때 물어보는 게 훨씬 쌉니다
· 뒤로 가면 어떻게 되는가 — 프로토타입에서 자주 비어 있는 자리이고, 실제로 자주 버그가 나는 자리이기도 합니다
시안을 받아서 "화면이 몇 개네"가 아니라 "상태가 몇 개네"로 세는 것. 그게 개발자가 이 도구를 여는 실익입니다.
🎯 값이 아니라 이름을 주고받기
앞에서 색상 코드를 그대로 복사하면 안 된다고 적었는데, 그 해법이 요즘 도구에는 기능으로 들어와 있습니다. 변수(Variables) 라고 부르는 것이요.
#00962B라는 값에 brand/primary라는 이름을 붙여두고, 시안에서는 그 이름을 씁니다. 그러면 브랜드 색이 바뀔 때 정의 한 곳만 고치면 시안 전체가 따라 바뀝니다. 코드에서 CSS 변수나 디자인 토큰을 쓰는 것과 완전히 같은 발상이에요.
여기서 개발자가 얻는 게 하나 더 생깁니다. 시안이 이름 체계를 갖추고 있으면 코드의 이름 체계와 맞춰볼 수 있다는 것이요. 양쪽이 brand/primary라는 같은 이름을 쓰면, 색이 바뀌었을 때 어디를 고쳐야 하는지 찾을 필요가 없습니다.
반대로 시안에 이름이 없고 색상값만 흩어져 있으면, 그건 나중에 일괄 변경이 어려운 구조라는 신호입니다. 이것도 시안을 받은 날 이야기해두면 좋은 항목이고요.
🤝 그래서 어디까지가 개발자 몫인가
저자가 "작은 수정은 직접 처리"라고 쓴 대목에는 사실 답이 정해져 있지 않습니다. 팀마다 다르고, 그래서 정해두지 않으면 매번 애매해집니다.
기준을 하나 세운다면 이렇게 나눌 수 있습니다.
· 시안 파일을 고치는 일 — 오탈자, 이미 있는 컴포넌트를 갖다 쓰는 배치 정도면 개발자가 해도 무리가 없습니다
· 새 규칙을 만드는 일 — 없던 색을 추가하거나 간격 체계를 벗어난 값을 넣는 것은 디자인 시스템을 건드리는 일이라 혼자 결정하면 안 됩니다
"이미 있는 것을 쓰는가, 없는 것을 만드는가" 로 가르면 대체로 맞습니다. 그리고 이 구분은 코드에서 공통 컴포넌트를 다룰 때 쓰는 기준과 똑같아요.
📱 화면 크기를 시안에서 미리 보는 법
개발자가 시안을 열었을 때 가장 먼저 확인하면 좋은 게 하나 더 있습니다. 이 디자인이 몇 가지 화면 크기를 상정하고 있는가요.
시안이 한 가지 너비로만 그려져 있으면, 그 사이의 크기에서 어떻게 보일지는 정해진 게 없다는 뜻입니다. 그러면 구현하는 사람이 매번 추측해야 하고, 나중에 "이건 이렇게 되면 안 되는데"가 나옵니다.
그래서 물어볼 것이 셋입니다.
· 어느 너비를 기준으로 그렸는가 — 그 사이 구간은 어떻게 늘어나야 하는지
· 글자가 길어지면 어떻게 되는가 — 두 줄이 되는지, 말줄임표가 붙는지
· 목록이 비면 무엇을 보여주는가 — 앞서 말한 상태 이야기와 이어집니다
세 가지 다 시안에는 잘 안 그려지고, 구현할 때는 반드시 필요합니다. 그리고 이런 질문은 파일을 직접 열어본 사람만 할 수 있어요. 이미지로 받아 보면 "이 요소가 늘어나는 요소인지 고정인지"조차 알 수 없거든요.
저자가 꼽은 이점 중 "프로토타입 이해"와 "작은 수정은 직접 처리"가 결국 여기로 모입니다. 파일 안에 들어가야 물어볼 것이 보인다는 것이요.
⏱️ 배우는 데 드는 시간을 정직하게 보면
마지막으로 비용 이야기를 하나 하겠습니다. 저자가 "디자이너처럼 깊게 배울 필요는 없다"고 선을 그은 게 이 대목에서 값을 합니다.
필수로 꼽은 셋 — 파일 열기, 수치 확인, 댓글 — 은 실제로 한 시간이면 익힙니다. 클릭 몇 번이 전부예요. 그런데 그 한 시간이 없애는 왕복이 프로젝트 하나에서 수십 번입니다.
반면 있으면 좋은 것으로 꼽은 셋 — 와이어프레임, 컴포넌트, 프로토타입 — 은 시간이 더 듭니다. 그리고 팀에 디자이너가 있으면 굳이 안 해도 되는 것들이고요.
"어디까지"를 정해주는 글이 드문 이유가 이겁니다. 도구를 소개하는 사람은 대개 그 도구를 좋아해서 전부 알려주고 싶어 하거든요. 읽는 사람의 시간을 기준으로 선을 그은 게 이 글의 성격입니다.
📝 원문에서는 이렇게 선을 긋습니다
배울 범위를 두 단으로 나눠 적었습니다.
1. 프로젝트 열기 & 파일 구조 이해
2. Inspect 기능 활용
3. 코멘트 확인 & 작성
🔥 있으면 좋은 기능
1. 와이어프레임 제작
2. 컴포넌트 이해 & 재사용
3. 프로토타입 미리보기
세 번째 항목이 특히 실용적입니다. 파일 안에 댓글로 질문을 남기면 그 질문이 해당 요소에 붙어 있게 되니까요. 메신저로 스크린샷 찍어 보내고 "여기요"라고 하는 것보다 나중에 찾기가 훨씬 낫습니다.
큐레이터 노트
새 도구를 권하는 글은 많은데, 어디까지만 배우면 되는지 선을 그어준 글은 드뭅니다. "디자이너처럼 깊게 배울 필요는 없다"는 전제를 먼저 깔고 필수와 선택을 나눈 구성이 그래서 좋았어요. 읽는 사람의 시간을 아껴줍니다.
「자기 분야 바깥으로 한 걸음 나간 기록」이기도 합니다. 협업에서 실제로 값이 나오는 지식은 대개 이런 경계에 있어요.
💡 세 편을 겹쳐 읽으며
알아서 해주는 것에는 규칙이 있습니다
세 편이 서로 다른 층에 있는데 같은 장면을 봅니다.
한대천 님의 글에서는 JPA가 알아서 save() 두 번을 한 번으로 정리합니다. 주시원 님의 글에서는 도구가 알아서 여백 수치를 뽑아줍니다. 홍범영 님의 글에서는 학원이 알아서 코딩을 가르쳐준다고 여겨집니다.
셋 다 편합니다. 그리고 셋 다, 규칙을 모르면 예상 밖의 상황에서 멈춥니다.
flush()와 clear()가 사이에 들어가면 INSERT가 두 번 나가고, Inspect 값을 그대로 복사하면 반응형에서 무너집니다. 편의는 규칙 위에 서 있고, 규칙을 모르면 편의가 끝나는 지점도 모릅니다.
도구를 배우기 전에 그 도구가 대신하는 일을 봅니다
홍범영 님은 코딩 학원 이야기로 시작해 "코딩은 구현하는 도구일 뿐"이라고 씁니다. 주시원 님은 "피그마를 배운다고 디자이너가 되는 건 아니다"라고 씁니다.
두 문장이 같은 말입니다. 도구를 익히는 것과 그 도구가 다루는 문제를 이해하는 것은 다른 일이라는 것이요.
한대천 님의 글도 같은 자리에 있습니다. save()라는 메서드 하나를 쓸 줄 아는 것과, 그게 persist()와 merge() 중 무엇을 부르는지 아는 것은 다릅니다. 쓸 줄 아는 것에서 예측할 수 있는 것으로 가는 게 이 회차의 공통 동작입니다.
셋 다 다음 편이 있습니다
한대천 님의 글은 "다시 정리하려고 합니다"로 열립니다. 홍범영 님은 열 편의 목차를 미리 걸었습니다. 주시원 님은 기초를 정리하고 다음 단계를 남겨뒀습니다.
기수 첫 회차의 글이 대개 그렇습니다. 완결이 아니라 선언입니다. 그래서 이 세 편은 뒤 회차의 글들과 겹쳐 읽을 때 값이 커집니다. 실제로 홍범영 님의 연재는 4회차에서 다시 만나게 됩니다.
이번 회차에서 가져갈 것
1. 이상해 보이는데 정상인 장면을 그냥 넘기지 않기.
save() 두 번에 INSERT 한 번은 버그가 아닙니다. 하지만 "정상이네"로 끝내면 대량 배치에서 메모리가 터질 때 원인을 못 찾습니다. 이상함을 느낀 순간이 원리로 내려갈 유일한 입구입니다.
2. 조건을 하나만 바꿔 결과를 갈라 보기.
원문의 flush() + clear() 실험이 그렇습니다. 설명 열 줄보다 재현 가능한 실험 두 줄이 강합니다. 그리고 그 실험은 읽는 사람이 자기 환경에서 다시 돌려볼 수 있습니다.
3. 대량 처리에서는 주기적으로 영속성 컨텍스트를 비우기.
반복문 안에서 save()만 부르면 1차 캐시가 계속 커집니다. 일정 개수마다 flush() + clear()를 넣으세요. 벌크 연산 뒤에도 clear()가 필요합니다.
4. Inspect 값은 정답 코드가 아니라 치수 도면으로 보기.
절대좌표·날것의 색상값·다른 폰트 렌더링 때문에 그대로 복사하면 대개 깨집니다. 대신 오토 레이아웃 구조를 읽으면 화면이 레이아웃 트리로 보입니다.
5. 시안은 화면 수가 아니라 상태 수로 세기.
로딩·성공·빈 목록·오류가 각각 상태입니다. 빠진 상태를 구현 중에 발견하는 것보다 시안을 받은 날 물어보는 게 훨씬 쌉니다.
4기 1회차는 2025년 9월, 기수가 막 열리던 무렵이었습니다.
프레임워크 안쪽과 학습의 순서와 협업 도구라는 서로 다른 자리에서, 세 분이 나란히 알아서 해주던 것을 한 번 열어보는 기록을 남겼습니다. 첫 회차다운 글들이에요.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.
좋은 글 남겨주신 한대천 님, 홍범영 님, 주시원 님께 감사드립니다. 그리고 4기를 함께 채워주신 모든 멤버분들께도요! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.
'성장일지' 카테고리의 다른 글
| [GL 4기 2회차 ②] 일을 떼어내 다른 데로 보내기 — 서브프로그램과 모듈, Celery 작업 큐, PySpark 🌱 (0) | 2026.08.11 |
|---|---|
| [GL 4기 2회차 ①] 실행을 다른 곳으로 옮기기 — 브라우저 C 컴파일, 게으른 시퀀스, Web Worker 🌱 (0) | 2026.08.11 |
| [GL 4기 1회차 ①] 이름을 붙이는 일과 나누지 않는 판단 — 시맨틱 태그, 리액트 리스트 렌더링 🌱 (0) | 2026.08.11 |
| [GL 3기 10회차 ③] 내보내기 직전의 기록 — 시니어 앱 MVP, 스토어 결제 연동, 세미나 준비 🌱 (0) | 2026.08.09 |
| [GL 3기 9회차 ③] 규칙만 남기고 맡기기 — Strimzi 오퍼레이터, C# 델리게이트 콜백, 반응형 레이아웃 🌱 (0) | 2026.08.09 |