I wrote a PHP file. Opened it in the browser. The screen was completely white.
In the assembly world, errors were clear. The carry flag would be set. A value would appear in the status register. You could tell what went wrong by reading the registers. But in the web world, errors manifest as "nothing being displayed." A single missing semicolon silences the entire screen.
It took me 3 hours to learn about display_errors = On in php.ini.
// The first rule I learned after 3 hours of blank screen
ini_set('display_errors', 1);
error_reporting(E_ALL);
// In assembly, HLT would stop and tell you.
// PHP just dies silently.
That day, I realized: debugging in web development is "the art of listening to silence."
CORS — The Invisible Wall
When I tried to connect kitemir.jp's virtual try-on feature with shop.kitemir.jp, the most confusing obstacle was CORS.
There's no concept of domains in the assembly world. Specify a port, send data, and the recipient receives it. But in the browser world, "you can't pass data because the domains are different, even though you own both servers" actually happens.
Access-Control-Allow-Origin: https://kitemir.jp
// ↑ It took 2 days to write this single line in .htaccess.
// 2 days of rage and confusion over
// "why my own servers are refusing to talk to each other."
The moment I understood CORS, the design philosophy behind web security became visible. Browsers demand trust relationships between servers to protect users. As a communication protocol designer, it clicked: "Ah, so that's the handshake."
Two days of frustration became one second of understanding. CORS wasn't an enemy — it was a design.
Async — A Paradigm Shift
Assembly is inherently synchronous. Call INT 13h and the CPU waits until the disk read finishes. CALL a subroutine and the next instruction doesn't execute until RET. Everything proceeds in order.
When I encountered JavaScript's asynchronous processing, the world flipped upside down.
// My first (wrong) code
let result = fetch('/api/tryon');
console.log(result); // → Promise { <pending> }
// I shouted "Why can't I get the data?!"
// fetch returns a "promise," but the data hasn't arrived yet.
// Correct code
const response = await fetch('/api/tryon');
const data = await response.json();
console.log(data); // → { success: true, ... }
Promise. A promise. "I don't have the result yet, but I'll tell you when it's done." A concept that didn't exist in the assembly world.
But thinking about it, there was something similar: interrupt handling. Request processing from hardware, and when it's done, get notified via IRQ. The essence of async was the same. The web just implemented it in a more refined way.
> INT 13h = synchronous disk read (CPU waits)
> fetch() = asynchronous HTTP request (Promise)
> IRQ = hardware callback
> .then() = software callback
>
> // Different names, same essence.
Stripe Webhook Signature Verification — Proof of Trust
When I integrated Stripe for payments, I stumbled on webhook signature verification. When Stripe sends a payment completion notification, it generates an HMAC-SHA256 signature from the request body and a secret key. The receiver verifies the signature to confirm "this really came from Stripe."
You can skip the verification and it still works. No problems in test environments. But in production, anyone could send a fake webhook claiming "payment completed."
When I understood this, my communication protocol engineer instincts fired up. HMAC signatures have the exact same structure as the packet checksum verification I used to implement. Computing a hash with a shared secret to guarantee data integrity. The same principle from 40 years ago, used right here.
The surface of technology changes. But the underlying principle of "proof of trust" hasn't changed in 40 years.
500 Internal Server Error — The Five Most Terrifying Characters
I can't count how many times I saw this number during development. 500. Internal Server Error. No cause displayed. Digging through logs, inserting var_dump line by line, hunting down where the process dies.
One day, when integrating with FASHN AI (virtual try-on API), responses stopped coming back. Status code 522. Timeout. The API itself was fine. My server was fine. Yet nothing worked.
The cause was a change in the API's request format. The mode parameter had moved from inside inputs to the top level. One level of hierarchy difference, and the whole service dies.
// ❌ Old format (522 error)
{
"inputs": {
"model_image": "...",
"garment_image": "...",
"category": "tops",
"mode": "performance" // ← putting it here kills it
}
}
// ✅ New format (works correctly)
{
"model_name": "tryon-v1.6",
"inputs": {
"model_image": "...",
"garment_image": "...",
"category": "tops"
},
"mode": "performance" // ← must be top-level
}
When integrating with external APIs, even perfect code can fail. Spec changes, downtime, rate limits. Uncontrollable variables always exist. This was the same as hardware control. Wrong IC datasheets, different behavior between production lots — I'd experienced all of it during my career.
Error Logs Were the Best Textbook
Looking back, most of what I learned about web development came from errors. Successful code only teaches you "it works." But errors teach you "why it doesn't work," "what's missing," and "what rules the web world runs on."
A blank white screen taught me PHP error handling. CORS taught me the browser security model. Promises taught me the philosophy of async programming. 522 errors taught me how to work with external APIs.
Web development, started at age 68. The number of errors probably exceeds several thousand. But each one made me a little bit stronger.
> error_count++;
> experience_points += error_count;
> level_up();
>
> // Output: Level 68 Engineer — Web Development Unlocked
$ git log --grep="fix" --oneline | wc -l
89
// 89 fix commits.
// Each one was a page in the textbook.
첫 번째 에러는, 새하얀 화면이었다
PHP 파일을 작성했다. 브라우저에서 접속했다. 화면은 새하얗게 비어 있었다.
어셈블러의 세계에서는 에러가 명확했다. 캐리 플래그가 세워진다. 상태 레지스터에 값이 들어간다. 무엇이 잘못되었는지는 레지스터를 보면 알 수 있었다. 하지만 웹의 세계에서는 에러가 "아무것도 표시되지 않는" 형태로 나타난다. 세미콜론 하나가 빠졌을 뿐인데, 화면이 침묵한다.
php.ini의 display_errors = On을 알게 된 것은 3시간 후였다.
// 3시간의 공백 끝에 배운 첫 번째 규칙
ini_set('display_errors', 1);
error_reporting(E_ALL);
// 어셈블러라면, HLT로 멈춰주었다.
// PHP는, 아무 말 없이 죽는다.
이 날, 깨달았다. 웹 개발에서의 디버깅이란, "침묵을 듣는 기술"이다.
CORS라는 이름의, 보이지 않는 벽
kitemir.jp의 가상 피팅 기능을 shop.kitemir.jp과 연동하려고 했을 때, 가장 이해하기 어려웠던 것이 CORS였다.
어셈블러의 세계에 도메인이라는 개념은 없다. 포트 번호를 지정하고 데이터를 보내면 상대가 받는다. 하지만 브라우저의 세계에서는 "같은 서버의 주인인데, 도메인이 다르기 때문에 데이터를 전달할 수 없다"는 사태가 벌어진다.
Access-Control-Allow-Origin: https://kitemir.jp
// ↑ 이 한 줄을 .htaccess에 쓰는 데 2일이 걸렸다.
// "왜 내 서버끼리 통신이 거부당하는 거지?"라는
// 분노와 혼란의 2일이었다.
CORS를 이해한 순간, 웹의 보안 모델 설계 사상이 보였다. 브라우저는 사용자를 보호하기 위해 서버 간의 신뢰 관계를 요구한다. 통신 프로토콜 설계자로서 "아하, 그런 핸드셰이크구나"라고 납득이 갔다.
분노의 2일이 이해의 1초로 바뀌었다. CORS는 적이 아니라, 설계였다.
비동기라는, 패러다임 시프트
어셈블러는 본질적으로 동기적이다. INT 13h를 호출하면 디스크 읽기가 끝날 때까지 CPU는 기다린다. CALL하면 RET가 올 때까지 다음 명령은 실행되지 않는다. 모든 것이 순서대로 진행된다.
JavaScript의 비동기 처리를 만났을 때, 세계가 뒤집혔다.
// 내가 처음 쓴 (잘못된) 코드
let result = fetch('/api/tryon');
console.log(result); // → Promise { <pending> }
// "왜 데이터가 안 나와?!"라고 외쳤다.
// fetch는 "약속"을 돌려줄 뿐, 데이터는 아직 도착하지 않았다.
// 올바른 코드
const response = await fetch('/api/tryon');
const data = await response.json();
console.log(data); // → { success: true, ... }
Promise. 약속. "지금은 아직 결과가 없지만, 끝나면 알려줄게"라는 개념. 어셈블러의 세계에는 존재하지 않았던 것이다.
하지만 생각해보면 비슷한 구조는 있었다. 인터럽트 처리다. 하드웨어에 처리를 의뢰하고, 완료되면 IRQ로 통지를 받는다. 비동기의 본질은 같았다. 웹은 그것을 보다 세련된 형태로 구현하고 있었다.
> INT 13h = synchronous disk read (CPU waits)
> fetch() = asynchronous HTTP request (Promise)
> IRQ = hardware callback
> .then() = software callback
>
> // 이름이 다를 뿐, 본질은 같았다.
Stripe Webhook 서명 검증 — 신뢰의 증명
결제 시스템에 Stripe를 도입했을 때, Webhook의 서명 검증에서 막혔다. Stripe는 결제 완료를 통지할 때, 요청 본문과 비밀 키로 HMAC-SHA256 서명을 생성해서 보내온다. 수신 측은 그 서명을 검증하여 "이것은 정말 Stripe에서 온 데이터다"라고 확인한다.
서명 검증을 건너뛰어도 동작한다. 테스트 환경에서는 문제없다. 하지만 본번에서 그러면, 누구든 가짜 Webhook을 보내 "결제 완료"를 위장할 수 있다.
이것을 이해했을 때, 통신 프로토콜 엔지니어로서의 피가 끓었다. HMAC 서명은, 예전에 내가 구현했던 통신 패킷의 체크섬 검증과 완전히 같은 구조다. 데이터의 무결성을 보장하기 위해, 공유 비밀 키로 해시를 계산한다. 40년 전과 같은 원리가 여기서도 사용되고 있었다.
기술의 표면은 변한다. 하지만 그 아래에 있는 "신뢰의 증명" 원리는, 40년간 변하지 않았다.
500 Internal Server Error — 가장 무서운 5글자
개발 중에 이 숫자를 몇 번 봤는지 모른다. 500. 서버 내부 에러. 원인이 표시되지 않는다. 로그를 뒤지고, 한 줄씩 var_dump를 끼워 넣으며, 어디서 처리가 죽었는지를 특정한다.
어느 날, FASHN AI (가상 피팅 API)와의 연동에서 응답이 돌아오지 않았다. 상태 코드 522. 타임아웃. API 자체는 정상. 내 서버도 정상. 그런데 동작하지 않는다.
원인은 API의 요청 포맷이 변경되었기 때문이었다. mode 파라미터의 위치가 inputs 안에서 최상위 레벨로 바뀌어 있었다. 단 1계층의 차이로 서비스가 멈춘다.
외부 API와의 연동에서는, 자신의 코드가 완벽해도 동작하지 않을 수 있다. 사양 변경, 다운타임, 속도 제한. 제어할 수 없는 변수가 항상 존재한다. 이것은 하드웨어 제어 때와 같았다. IC 사양서가 틀려 있거나, 로트에 따라 동작이 다르거나, 현역 시절에 경험했던 일이었다.
에러 로그는, 최고의 교과서였다
돌이켜 보면, 내가 웹 개발에서 배운 것의 대부분은 에러에서 배웠다. 성공한 코드에서는 "그것이 동작한다"는 것밖에 배울 수 없다. 하지만 에러에서는 "왜 동작하지 않는지", "무엇이 부족한지", "웹의 세계는 어떤 규칙으로 동작하는지"를 배울 수 있다.
새하얀 화면이 PHP의 에러 핸들링을 가르쳐 주었다. CORS가 브라우저의 보안 모델을 가르쳐 주었다. Promise가 비동기 프로그래밍의 사상을 가르쳐 주었다. 522 에러가 외부 API와의 관계 방식을 가르쳐 주었다.
68세부터 시작한 웹 개발. 에러의 수는 아마 수천을 넘었을 것이다. 하지만 그 하나하나가 나를 조금씩 강하게 해주었다.
> error_count++;
> experience_points += error_count;
> level_up();
>
> // 출력: Level 68 Engineer — Web Development Unlocked
$ git log --grep="fix" --oneline | wc -l
89
// 89번의 수정 커밋.
// 그 하나하나가, 교과서의 한 페이지였다.