FRONTEND / COMPONENT
Replacing a Commercial Data Grid With an In-House Component
Every admin list screen hand-rolled its own render loop and paging math, and the rest were locked to a commercial grid licensed per server. I built an in-house grid that absorbs the existing response contracts as-is, then moved roughly 80 screens onto it without changing a single line of server code.
- Scope
- 80+ screens
- Server changes
- 0
- Net code
- -733 lines
- Regressions
- 0
lists · editors · picker modals
controllers · JSON untouched
measured on first 6 screens
127 specs · 39-screen battery
Problem
The same code copied once per screen — and the rest tied to a license
Badabom's admin has dozens of list screens, and no single grid behind them. There were essentially two camps. One was hand-written tables: each screen built <tr> strings in a for loop, copied the pagination markup, computed the descending row-number offset by hand, and assembled a separate array for Excel export. The other was a commercial grid — functional, but licensed per server (IP) with a domain cap, and any environment on a lapsed license showed a trial watermark on screen.
The real cost was not line count but bug surface area. Paging offsets, XSS escaping, Excel-label drift — the exact code people get wrong existed as one copy per screen. A bug found in one place had to be hunted down in all the others. Add a column and you had to edit both the screen code and the Excel builder; someone editing only one of them did actually happen.
// Before — repeated on every screen. Each list carried the whole block.function drawList(list, totalCnt) { var html = ''; for (var i = 0; i < list.length; i++) { var r = list[i]; var no = totalCnt - ((pageNo - 1) * PER_PAGE) - i; // descending row no — easy to get wrong html += '<tr onclick="goDetail(\'' + r.techSn + '\')">' + '<td>' + no + '</td>' + '<td class="tit">' + esc(r.techNm) + '</td>' // forget esc() and it is an XSS hole + '<td>' + (r.regDt || '').substring(0, 10) + '</td>' + '<td>' + badge(r.aprvYn) + '</td>' + '</tr>'; } $('#listBody').html(html || '<tr><td colspan="4">No results.</td></tr>'); drawPaging(totalCnt, pageNo); // pagination markup copied per screen too $('#totCnt').text(totalCnt);} // + a separate array-of-arrays builder for Excel export (labels tracked apart from the// on-screen headers, so adding a column meant editing both — and people edited one)// + sorting/filtering mostly unimplemented; adding it meant another hand-written copyRender logic duplicated once per screen
Each list shipped a render loop, pagination markup, total-count update, and Excel assembly as a set. Fixing one bug meant fixing it as many times as there were screens.
XSS escaping depended on the developer remembering
Building rows as strings means a missing esc() call is a live hole. That review item repeated on every screen.
Commercial grid: per-server license plus a domain cap
Adding a server or a domain meant re-running the license math, and dev environments showed the trial watermark right in the UI.
Sorting, filtering, and export were inconsistent
Some screens sorted, some did not, and users kept asking why. Adding it by hand only produced more copies — a dilemma the component was meant to end.
Approach
Designing around one hard constraint: do not touch the server
Buying another commercial grid or adopting an open-source one were both on the table. But both typically ask you to reshape your responses to fit the grid. On an iBATIS + JSP legacy stack, rewriting the response JSON of dozens of controllers is not a risk this system could absorb. So I fixed the first constraint up front: zero lines of server code changed.
I started by cataloguing the admin list ajax contracts to count how many response shapes were actually in use. Three. So the grid got a thin adapter layer — envelope presets — and screens just name their shape; anything unusual passes a function instead. That one layer means the server keeps returning exactly what it already returns.
Before building, I ran a gap analysis across 14 commercial and open-source grids to separate "features we actually use" from "features that only exist in the brochure." That became a P0–P2 roadmap: sorting and paging first, edit mode and virtual scrolling deferred. Trying to build everything up front would never have shipped.
Delivery matched the legacy stack too: no build tooling, one JS file and one CSS file, with the admin console decorator loading both globally. Improve the component once and it lands on every grid at the same time, with no WAR rebuild.
LOAD
ajax response
legacy JSON as-is
envelope
shape adapter
transform
derived-field hook
DERIVE → RENDER
filter
quickFilter · filterRow
sort
client all / server page
window
virtual scroll slice
render
keyed reconciliation
Envelope adapter
Grid absorbs 3 response shapes — controllers and SQL untouched
No-build deploy
Copy JS/CSS; the decorator loads it on every screen
Gap analysis first
14 grids surveyed, then P0–P2 priorities — not everything got built
Process
How it was built
- 01
Collapsing screens into a column declaration
What a screen actually does is one sentence: query this URL with this form, and draw these columns. So the module took over the render loop, pagination, empty/error/loading states, total-count updates, and Excel assembly — leaving only a
columnsdeclaration on the page. Every default renderer escapes, which moved XSS from "the developer remembered" to a component contract. Excel labels reuse the column titles, so there is nothing left to keep in sync.admTechInfoList.jsp — afterjavascript// After — same screen. It collapses into one declaration, and the endpoint is untouched.var grid = new BdbGrid('#techGrid', {url: '/adm/techTrade/techInfo/data.do', // existing endpoint, unchangedform: '#listForm', // serialize this form as the requestenvelope: 'listTotal', // just name the response shapepaging: 'server',perPage: 10,caption: 'Technology listings', // accessibility caption (sr-only)rowKey: 'techSn',totalEl: '#totCnt',columns: [{ title: 'No.', type: 'rownum', width: 64 },{ title: 'Title', field: 'techNm', type: 'title', width: '32%', sortable: true,href: function (r) { return '/adm/techTrade/techInfo/edit/view.do?techSn=' + r.techSn; } },{ title: 'Created', field: 'regDt', type: 'date', format: 'YYYY-MM-DD', width: 110 },{ title: 'Approved', field: 'aprvYn', type: 'badge', width: 96,badge: { 'Y': 'ok', 'N': 'no' } }],excel: { fileName: 'tech-info' } // Excel labels = column titles, nothing to sync});$('#searchBtn').on('click', function () { grid.reload(); }); - 02
The envelope adapter — where the server stays untouched
Surveying the admin list responses turned up three families: the standard
{response:{list,totalCnt}}, the array form{response:[...]}, and a small tail. The first two became presets; the tail passes a function. I deliberately kept "interpreting the envelope" and "preprocessing the data" as separate hooks — merge them and a change in response shape breaks your derived-field logic along with it.bdbGrid.jsjavascript// Envelope presets — the one place that absorbs legacy response shapes, so the server never movesvar ENVELOPES = {// {response:{list, totalCnt}} — the server-paging standardlistTotal: function (res) {var r = (res && res.response) || {};var list = r.list || [];return {list: list,totalCnt: (r.totalCnt !== undefined && r.totalCnt !== null)? Number(r.totalCnt) : list.length};},// {response:[...]} — response is the array itself (full-load screens)array: function (res) {var arr = (res && res.response) || [];if (!isArray(arr)) arr = [];return { list: arr, totalCnt: arr.length };}};BdbGrid.prototype._parse = function (res) {var env = this.opts.envelope;var fn = (typeof env === 'function') ? env : ENVELOPES[env]; // third shape: pass a functionif (!fn) fn = ENVELOPES.listTotal;var out = fn(res) || {};return {list: out.list || [],totalCnt: (out.totalCnt != null) ? Number(out.totalCnt): (out.list ? out.list.length : 0)};}; - 03
Discarding stale responses, resetting edit and sort state
What actually breaks in production is never the flashy feature. Click search twice quickly and the earlier request can arrive last, overwriting the screen with older data. Each request carries a sequence number, and anything that is no longer the newest gets dropped. In the same spirit, new data resets edit state and sort order — while client-side page switches, which never hit ajax, keep the edit state intact.
bdbGrid.jsjavascript// Discarding stale responses — hit search twice quickly and the older reply can land lastvar reqId = ++this._req; // render only while this request is still the newest$.ajax(ajax).done(function (res) {if (reqId !== self._req) return; // a newer request went out — drop this replyvar out = self._parse(res);if (o.transform) out.list = o.transform(out.list, self) || out.list; // derived-field hookif (o.tree) out.list = self._treeFlatten(out.list); // tree: DFS flattenself._resetEdit(); // new data means edit state resetsif (o.paging === 'client') {self._all = out.list;self._resetSort(); // new data means a new original orderself.total = out.totalCnt;self._clientPage(page);} else {self.total = out.totalCnt;self._render(out.list);}}); - 04
From read-only to editing — the condition for retiring the commercial grid
Replacing only the lists never retires a commercial grid; what is left is always the editors. So inline editors, C/U/D dirty tracking, derived bulk-save payloads, undo/redo, Excel paste, frozen columns, column show/hide, grouping and tree views all moved into the component — features that usually sit in a higher license tier on commercial products. Only after that could the editing screens migrate.
- 05
Accessibility — tables for reading, widgets only for editing
This is a public-sector site, so web accessibility is a gate, not a nice-to-have. Read-only lists deliberately stay static-table semantics —
captionplusth scope— so screen readers keep their native table navigation, and only editing screens becomerole="grid"with roving tabindex. Sort state is announced viaaria-sort, query and validation results viaaria-live. Widgetizing everything looks more impressive, but for read-only lists it is a net loss. - 06
A regression net: spec runner plus click battery
The decorator loads this component on every admin screen, which means one component bug is a site-wide outage. So I kept a standing spec runner accumulating assertions and ran a separate battery that walks real screens in a real browser and clicks through them. Each migration batch only proceeded after confirming zero battery regressions following a major version bump. Bulk migrations ran as parallel Claude Code agent batches, with adversarial code review assigned to separate agents as a cross-check.
Outcome
Results and takeaways
Net code reduction per migration batch (first 6 screens, measured)
lines
tech info · deals · showcase
-408
exhibition ×3 (video · event · cmrcl)
-325
total
-733 (995 removed / 262 added)
Front end swapped with zero server changes
Controllers, endpoints, and response JSON stayed exactly as they were. With backend risk at zero I could push the migration in batches and roll back a single screen if something went wrong. Without that property, this scale of change on a live system would not have been attempted.
Bug surface went from screens × N down to one
Paging offsets, XSS escaping, Excel-label sync — the error-prone code now lives in one component. That mattered more than the -733 lines: fix it once and every screen gets the fix.
License and watermark dependency removed
Adding a server or a domain no longer triggers a license recalculation, and the trial watermark is gone from dev environments. It also bought control: neither an abandoned open-source grid nor a vendor license policy change can move this stack now.
The key was deciding what not to build
What the 14-grid gap analysis produced was not a feature list but a list of features we do not use. Chasing the full brochure would still be unfinished today. P0 covered what was genuinely in use; the rest got added when something actually needed it.
What is still outstanding, honestly
Hand-written tables remain on some screens, and a few shared picker modals still run the commercial grid. The UX is identical, so those are being converted opportunistically when the screen is next touched. Meanwhile the enhancers (tooltips, column resize) apply to unconverted tables too, which narrows the gap in the interim.
MORE
Explore other cases
Badabom
AUTH / SSO
Building an SSO Provider for Partner Sites
Implemented an SSO Provider so external partner sites (e.g., OTT) could sign in with Badabom accounts. Single-use UUID tokens stored in the database support multiple WAS nodes, and CI (Connecting Information) auto-maps accounts across both sides.
View detailBadabom
DEVOPS / OBSERVABILITY
SSE + Cross-WAS Real-Time Log Viewer
The WAS lived in the Daejeon IDC, but network-segregation policy meant only Busan-office PCs could reach it — so pulling a log effectively meant flying to Busan. I built an SSE-based viewer inside the admin web and added a cross-WAS relay so logs from both WAS nodes stream into a single screen.
View detailBadabom
LEGACY MIGRATION
Migrating the OTT Technology-Trade System into Badabom
Moved an Oracle + MyBatis technology-trade platform (OTT) onto PostgreSQL + iBATIS. Rewrote 87 URLs, 34 JSPs, 80+ SQL queries, and 14 tables.
View detailBadabom
LEGACY UI / ARCHITECTURE
Wrapping 106 Admin Screens in a Console Shell Without Touching Them
The admin menu had to become a separate-window console, but rewriting ~100 /adm screens was off the table. I converted the existing SiteMesh decorator in place into a child decorator so every current mapping followed automatically, kept the topbar and sidebar resident in a parent shell, and swapped only the iframe body.
View detailGAIS — Government Advertising Integrated Support System
CI/CD
Automating the Build and Deploy Pipeline
Replaced a fully manual build-and-deploy workflow with a Jenkins + GitLab Webhook pipeline, cutting deploy time from 15–20 min down to around 4 min.
View detailGAIS — Government Advertising Integrated Support System
INFRA / SESSION
Redis-Backed Session Clustering
JEUS Standard doesn't support native session clustering, so I put Redis in front as an external session store. That unlocked rolling restarts across WAS nodes.
View detailGAIS — Government Advertising Integrated Support System
SECURITY / NETWORK
Applying TLS 1.3 via an Nginx Reverse Proxy
Touching the shared WebtoB SSL felt risky, so I put Nginx in front and terminated TLS there instead. Existing services kept running untouched while TLS 1.3 was rolled out.
View detailFreelance · Side Projects
CLIENT WORK / WEB
Pitched and Built a Postpartum Care Center Site Renewal
My wife had stayed at a postpartum care center whose website felt dated, so I mocked up a UI sample and pitched it myself. I built an Astro static site with a 192-frame scroll animation, Kakao Map, and SEO — then shipped it to their production domain.
View detailFreelance · Side Projects
SIDE PROJECT / AI
Family-Driven Baby Naming with AI + Tournament-Style Voting
Existing naming services are designed for solo use, so I built a way for the whole family to join in. GPT-4o suggests names aligned with Saju (birth-chart) and Ohaeng (Five-Element) rules, and the family votes tournament-style to pick the final name.
View detail