픽셀아트Canvas시각화

부캉이는 수로를 뱅뱅 돌았다

2026.09.26

비 내리는 밤 부산 북항을 옆에서 본 도트 그림. 뒤로 부산항대교와 불 켜진 도심, 데크 난간 너머로 우산을 쓴 구경꾼이 빽빽하다. 오른쪽 위 롯데 전광판에 「TODAY - : - W6」. 물속에서 상어가 왼쪽으로 헤엄치고 등지느러미 끝이 수면 위로 나와 있다. 왼쪽 위 기록판에 「09.26 토요일 · 수로에서 9일째 · 누적 32.3만 명」

9월 18일 아침, 부산 북항 친수공원 수로에 몸길이 3.5m 상어가 들어왔다. 무태상어로 추정된다. 휘어진 수로에서 출구를 못 찾고 같은 자리를 뱅뱅 돌았다.

부산시설공단이 「부캉이」라는 이름을 붙였고, 추석에는 하루 13만 6천 명이 보러 왔다. 공교롭게 그날부터 롯데가 연승을 시작해서 「부캉아 가지 마」라는 말까지 나왔다.

이걸 KBO 순위 레이스와 같은 틀에 담았다. 발견한 날부터 오늘까지 하루씩 넘기다가 끝나면 처음으로 돌아가는 루프다. 재생·날짜 이동·배속이 있고, 그날의 구경꾼과 사건과 롯데 경기가 기록판에 뜬다.

부캉이의 기록 보러 가기

뉴스를 날짜로 쪼갰다

기사는 대개 「지금까지」를 말한다. 「누적 32만 명」, 「5연승」. 그걸 그날그날로 되돌리는 게 이 페이지의 일이었다.

9일의 기록
날짜
구경꾼
그날 일
롯데
18 (금)
—
수로에서 발견, 해경이 몰아내려다 멈춤
승 9:6 NC
19 (토)
8,800
소문이 돌기 시작
승 5:3 삼성
20 (일)
1만
숭어를 쫓아 들어왔다는 분석
승 13:6 삼성
21 (월)
1만 3천
편의점 죠스바 28%↑
경기 없음
22 (화)
1만 7천
«부캉이» 이름, 연휴까지 기다리기로
승 7:0 한화
23 (수)
3만 7천
하루 만에 두 배
승 9:5 한화
24 (목)
10만 1천
추석 연휴 첫날
승 6:4 LG
25 (금)
13만 6천
추석, 가장 많은 날
경기 없음
26 (토)
—
비, 누적 32만 명 넘음
기록 대기

구경꾼 숫자는 부산시설공단이 낸 집계만 썼다. 여러 기사에 흩어져 있는 걸 날짜별로 모았다.

없는 숫자는 비워 뒀다

18일과 26일은 집계가 없다. 18일은 아직 아무도 세지 않았고, 26일은 오늘이라 아직 안 나왔다.

그래서 비워 뒀다. 기록판에는 「집계 전」, 「집계 중」이라고 쓰고, 그래프 막대 자리에는 빗금과 물음표를 넣었다.

그럴듯한 숫자를 채우기는 쉽다. 19일이 8,800명이니 18일은 3천쯤이겠지. 그런데 그 순간 이 페이지는 기록이 아니라 짐작이 된다. 옆에 있는 진짜 숫자들까지 같이 의심받는다.

다만 그림은 비워 둘 수가 없다. 난간 너머가 텅 비면 그것도 거짓말이다. 그래서 숫자와 그림을 떼어 놓았다.

  • 18일 그림은 평소 친수공원 하루 방문객 2천 명(부산시설공단)으로 그린다
  • 26일 그림은 앞날 인파를 그대로 쓴다 — 비가 와도 우산 행렬이었다는 보도가 있다
  • 이 숫자들은 화면에 안 띄운다. 그림에만 쓴다

페이지 밑에도 「인파 그림은 숫자를 줄여 그린 것이다」라고 적어 뒀다. 포수 시점 편에서 회전수를 추정치라고 화면에 적은 것과 같은 이유다.

기사 숫자를 테스트로 묶었다

날짜별 숫자를 모으면 기사가 말한 합계와 맞아야 한다. 맞는지 확인하는 테스트를 뒀다.

it("19~25일 방문객을 더하면 보도된 누적 32만 명이 된다", () => {
  expect(at("20260925").cum).toBe(322_800);
});
 
// 스타뉴스 2026-09-24: 18~23일 5연승, 5경기 43득점, 58승 2무 71패 8위.
it("롯데 연승·득점·순위가 기사와 같다", () => {
  expect(run.reduce((n, g) => n + g.us, 0)).toBe(43);
  expect(at("20260923")).toMatchObject({ streak: 5, rank: 8, wl: "58승 2무 71패" });
  expect(at("20260924").streak).toBe(6);
});

8,800 + 1만 + 1만 3천 + 1만 7천 + 3만 7천 + 10만 1천 + 13만 6천 = 32만 2,800. 기사의 「32만 명 넘었다」와 맞는다.

롯데 쪽은 더 깐깐하다. 5경기 득점을 더하면 9 + 5 + 13 + 7 + 9 = 43. 기사의 「43득점 폭발」 그대로다. 순위와 승패도 같다.

숫자 하나를 옮겨 적다 틀리면 이 테스트가 먼저 운다. 사람이 눈으로 대조하면 아홉 줄 중 한 줄쯤은 그냥 넘어간다.

롯데는 적지 않고 읽었다

롯데 결과를 days.json에 손으로 적을 수도 있었다. 안 했다.

KBO 순위 레이스가 이미 KBO 기록실에서 받은 시즌 전 경기 결과를 들고 있다. 거기서 롯데 경기만 골라 읽으면 연승도 순위도 계산으로 나온다.

손으로 적기
경기 기록에서 읽기
데이터두 군데에 같은 결과
데이터한 군데
새 경기두 번 적는다
새 경기kbo:update 한 번이면 같이 채워진다
연승·순위기사 보고 옮긴다
연승·순위시즌 첫 경기부터 계산

같은 사실을 두 군데 적으면 언젠가 둘이 어긋난다. 그리고 어긋난 걸 발견하는 사람은 대개 독자다.

경기가 없던 날(월요일, 추석)에는 앞의 연승과 순위를 이어 쓴다. 26일처럼 아직 결과가 안 들어온 날도 마찬가지다. 전광판에는 「TODAY」를 띄우고, 연승은 마지막으로 아는 6연승을 보여 준다.

뱅뱅 도는 건 타원 하나다

부캉이 기사에서 제일 많이 나온 말이 「같은 자리를 뱅뱅 돈다」다. 그걸 그리는 데 필요한 건 타원 하나였다.

수로를 옆에서 본다고 치면, 원을 도는 상어는 옆에서 봤을 때 좌우로 왔다 갔다 하는 것처럼 보인다. 여기에 깊이를 준다.

  • 오른쪽으로 갈 때는 앞쪽, 깊은 곳을 지난다
  • 왼쪽으로 돌아올 때는 뒤쪽, 수면 가까이를 지난다
  • 양 끝에서 돌아설 때는 몸을 짧게 그린다 — 몸이 이쪽을 향해 돌아서는 순간이다
const th = (clock / LAP_MS) * Math.PI * 2;
const sx = cx + A * Math.sin(th);
const sy = 120 + 20 * Math.cos(th);   // 오른쪽으로 갈 땐 깊게, 돌아올 땐 수면 쪽으로

돌아오는 길에는 등지느러미 끝이 수면 위로 나온다. 물 위로 나온 부분은 선명하게, 물속은 물빛을 덮어 흐리게 그리고, 그 경계를 수면 높이에서 잘랐다. 지느러미가 올라오면 근처 구경꾼이 휴대폰 플래시를 터뜨린다.

한 바퀴는 15초다. 날짜 재생 속도와 상관없이 늘 같다. 4배속으로 날짜를 넘겨도 상어까지 4배로 빨리 돌면 다급해 보여서 뺐다.

상어를 찍지 않고 식으로 그렸다

확대한 도트 상어. 회갈색 등과 크림색 배, 뒤로 누운 등지느러미, 위 잎이 긴 꼬리지느러미, 가슴지느러미, 작은 눈과 입

도트 상어를 한 칸씩 찍어도 됐다. 그런데 필요한 그림이 한 장이 아니었다. 꼬리를 흔드는 네 장, 돌아서는 두 장. 여섯 장을 따로 찍으면 한 장을 고칠 때 다섯 장을 또 고친다.

그래서 몸통 윤곽을 식으로 잡았다. 몸 길이를 따라가며 두께를 정하는 함수 하나, 지느러미는 삼각형 여섯 개. 픽셀마다 「이 칸이 삼각형 안인가」를 물어서 칠한다.

  • 꼬리 흔들기는 꼬리지느러미 가로 폭만 1 → 0.7 → 0.35 → 0.7로 바꾼다. 옆에서 보면 꼬리를 흔드는 게 꼬리 폭이 줄었다 늘었다 하는 것으로 보인다
  • 돌아서기는 몸 길이만 40 → 22 → 12로 줄인다
  • 등은 회갈색, 배는 크림색으로 칠했다. 기사에서 「갈색빛」이라 했고, 위아래 색이 다른 건 실제 상어의 보호색이다

캔버스의 fill()로 삼각형을 칠하면 가장자리가 흐릿하게 번진다. 도트 그림에서는 그게 얼룩이라, 판정을 직접 했다. 열 줄짜리 함수 하나다.

구경꾼은 더해지기만 한다

첫날 화면. 데크 위 구경꾼이 서른 명쯤 띄엄띄엄 서 있고, 롯데 전광판에 「W 9:6 NC」, 기록판에 「구경꾼 집계 전」

추석 화면. 같은 데크가 구경꾼으로 빽빽하고, 머리에 주황 비닐봉지를 쓴 사람이 섞여 있다. 오른쪽 위에 보름달, 롯데 전광판에 「NO GAME W6」

구경꾼 자리는 처음에 240개를 정해 두고, 자리마다 순번을 매겼다. 그날 인파만큼 앞 순번부터 세운다.

그러면 날이 바뀔 때 사람이 더해지기만 한다. 어제 있던 사람이 자리를 바꾸거나 사라지지 않는다. 날마다 자리를 새로 뽑으면 인파가 늘어나는 게 아니라 화면이 깜빡이는 것처럼 보인다.

인원은 방문객 수에 제곱근을 씌워 정했다. 그대로 비례시키면 19일(8,800명)은 추석의 6%라 난간에 열다섯 명만 선다. 사람이 몰리기 시작했다는 느낌이 안 난다. 제곱근을 씌우면 예순 명쯤이 되고, 추석에 가서 꽉 찬다.

그 외에 날마다 다른 것 셋.

  • 추석(25일)에는 보름달
  • 26일에는 비 — 구경꾼이 전부 우산을 편다
  • 롯데가 연승 중이면 주황 비닐봉지 — 사직 응원 그대로 머리에 쓴다. 연승이 길수록 늘어난다

캡처가 안 돼서 링크를 만들었다

다 만들고 날짜별 화면을 확인하려는데 캡처가 말을 안 들었다.

  • 헤드리스 크롬은 애니메이션 시간이 제대로 안 흘러서 계속 18일에 머물렀다
  • 자동화용 브라우저 탭은 백그라운드라 requestAnimationFrame이 멈춰서, 날짜는 바뀌는데 인파는 바뀌는 도중에 얼어 있었다

두 번째 건 처음에 버그인 줄 알았다. 추석인데 난간에 사람이 스무 명 남짓이었다. 인파 값을 찍어 보니 0.14에서 멈춰 있었다. 코드가 아니라 탭이 잠들어 있었던 거다.

그래서 ?d=20260925 처럼 날짜를 붙이면 그날부터 트는 기능을 넣었다. 검증용으로 넣었는데, 넣고 보니 특정한 날을 링크로 건네는 기능이기도 했다. 「추석날 부캉이」를 보여 주고 싶으면 이 링크를 보내면 된다.

남은 것

이 페이지에서 내린 결정은 거의 다 기록과 그림을 떼어 놓는 일이었다.

  • 숫자는 보도된 것만, 없으면 비워 둔다
  • 그림은 비워 둘 수 없으니 채우되, 채운 숫자는 화면에 안 띄운다
  • 같은 사실은 한 군데에만 두고 읽어 온다
  • 기사의 합계는 테스트로 확인한다

하나 더. 이건 귀여운 이야기이기만 한 건 아니다. 전문가들은 이 상어가 14~22도 물을 좋아해서 수온이 떨어지면 수로에서 버티기 어렵다고 했다. 아열대 상어가 여기까지 올라온 것 자체를 경고로 봐야 한다는 목소리도 있었다. 28일에는 그물로 바다에 돌려보내는 방안을 놓고 관계기관 회의가 열린다.

그래서 이 기록은 부캉이가 바다로 나가는 날에 끝내기로 했다. 그날이 빨리 오면 좋겠다. 롯데 팬들한테는 미안하지만.

부캉이의 기록 보러 가기

같이 읽으면 좋은 글