
Frontend Developer
안녕하세요, 배채은입니다
불가능은 없다는 믿음으로, 언제나 새로운 도전에 나서는 프론트엔드 개발자입니다.
- Location
- Seoul, South Korea
- 01lily0302@naver.com
- GitHub
- Paicearea
Stack
Skills
제품 의도에 맞는 기술을 고르고, 유지보수 가능한 화면 단위로 구현합니다.
Core Frontend
사용자 화면을 빠르게 만들고 안정적으로 유지하는 기술
- React
- React Native
- Next.js
- TypeScript
- JavaScript
Build & Tooling
개발 속도와 배포 품질을 높이는 도구
- Webpack
- Vite
- Pnpm
- Electron
- Docker
Styling
컴포넌트 단위의 일관된 UI 구현
- Tailwind CSS
- Styled-Components
- Emotion
Product & Design
기획 의도를 화면 경험으로 연결하는 협업 역량
- Figma
- Jira
- Notion
- GitHub Actions
- Vercel
Work
Projects
기능 목록보다 사용자가 만나는 문제와 화면 흐름을 중심으로 정리했습니다.
04
앤딩
Mobile Frontend여러 유저들과 소설을 협업 창작하는 React Native 앱 서비스
협업 창작 경험을 위한 모바일 화면 흐름 구현
- React Native
- TypeScript
- Redux-Toolkit
- Styled-Components
- React-Hook-Form
06
모잉
Frontend산모와 보호자를 연결해주는 웹 서비스
민감한 사용자 여정을 고려한 정보 전달 화면 구현
- React
- TypeScript
- Emotion
- Chart.js
- Axios
Writing
Blog
학습과 프로젝트 과정에서 얻은 내용을 기록합니다.
Axios 대신 fetchBaseQuery를 선택한 이유와 마이그레이션 후기
들어가며 프론트엔드 개발을 하다 보면 거의 무조건 한 번쯤은 Axios를 사용하게 됩니다. 하지만 최근 프로젝트에서는 RTK Query의 내장 기능인 fetchBaseQuery로 완전히 전환했습니다. 이번 글에서는 왜 그렇게 했는지, 어떤 장단점이 있었는지, 그리고 실제 코드 예시를 통해 마이그레이션 방법을 공유하려고 합니다. 1️⃣ 기존 구조: Axios 기반 API 관리 기존에는 Axios 인스턴스를 만들어서 공통 헤더, 에러 핸들링, 인터셉터 등을 직접 설정했습니다. // api/axiosInstance.js import axios from 'axios'; const axiosInstance = axios.create({ baseURL: process.env.REACT_APP_API_URL, timeout: 5000, }); axiosInstance.interceptors.request.use((config) => { const token = localStorage.getItem('accessToken'); if (token) config.headers.Authorization = `Bearer ${token}`; return config; }); axiosInstance.interceptors.response.use( (res) => res, (err) => { if (err.response?.status === 401) { window.location.href = '/login'; } return Promise.reject(err); } ); export default axiosInstance; 이 방식은 유연하고 강력하지만, 문제는 Redux Toolkit Query(RTK Query)를 쓰기 시작하면서 복잡도가 커졌다는 점입니다. 2️⃣ RTK Query 도입 후의 고민 RTK Query는 데이터 요청, 캐싱, 리패칭 로직을 자동으로 관리해주는 훌륭한 도구입니다. 그런데 axios를 그대로 사용하려면 baseQuery를 직접 구현해야 했어요: const axiosBaseQuery = ({ baseUrl } = { baseUrl: '' }) => async ({ url, method, data, params }) => { try { const result = await axios({ url: baseUrl + url, method, data, params }); return { data: result.data }; } catch (axiosError) { const err = axiosError; return { error: { status: err.response?.status, data: err.response?.data } }; } }; 이렇게 하면 되긴 하지만, 이미 RTK Query는 fetch 기반의 기본 구현체인 fetchBaseQuery를 제공하고 있었죠. 결국 “굳이 Axios를 유지해야 할까?” 라는 고민이 생겼습니다. 3️⃣ fetchBaseQuery로 전환한 이유 비교 항목 Axios fetchBaseQuery 패키지 용량✅ 비교적 큼 (~12KB gzip) 작음 (fetch 내장 사용) RTK Query 통합 직접 baseQuery 작성 필요⚡️ 기본 제공 에러 핸들링커스터마이징 자유도 높음기본 에러 구조 제공 브라우저 호환성매우 넓음최신 브라우저 중심 인터셉터 기능지원 (요청/응답 가로채기)직접 래핑 필요 결국, 패키지 의존성 줄이기 RTK Query와의 통합성 향상 코드 단순화 이 세 가지 이유로 fetchBaseQuery로 전면 전환을 결정했습니다. 4️⃣ 전환 후 코드 예시 // api/baseApi.js import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'; export const baseApi = createApi({ reducerPath: 'baseApi', baseQuery: fetchBaseQuery({ baseUrl: process.env.REACT_APP_API_URL, prepareHeaders: (headers) => { const token = localStorage.getItem('accessToken'); if (token) { headers.set('Authorization', `Bearer ${token}`); } return headers; }, }), tagTypes: ['User', 'Post'], endpoints: (builder) => ({ getUser: builder.query({ query: (id) => `/users/${id}`, providesTags: ['User'], }), updatePost: builder.mutation({ query: (post) => ({ url: `/posts/${post.id}`, method: 'PUT', body: post, }), invalidatesTags: ['Post'], }), }), }); export const { useGetUserQuery, useUpdatePostMutation } = baseApi; ✅ 장점: 불필요한 Axios 의존성 제거 RTK Query와 완벽히 호환 자동 캐싱 / 리패칭 / 에러 핸들링 훨씬 간결한 코드 구조 5️⃣ 전환하면서 느낀 점 fetchBaseQuery의 커스터마이징 한계는 있습니다. 예를 들어, 요청을 재시도하거나 응답을 변환하는 로직은 직접 래핑해야 합니다. 하지만 RTK Query의 캐싱·동기화 기능과의 궁합이 훨씬 좋습니다. 대부분의 REST API 환경에서는 Axios의 추가 기능까지는 필요하지 않았습니다. 결론 작은 프로젝트이거나, RTK Query를 사용하고 있다면 fetchBaseQuery로 통일하는 게 훨씬 깔끔합니다. 반면, 여러 API 클라이언트를 병행하거나, 복잡한 인터셉터 로직이 있다면 여전히 Axios가 더 유리할 수 있습니다. ✨ 마무리하며 Axios는 여전히 훌륭한 HTTP 클라이언트입니다. 하지만 RTK Query를 중심으로 상태 관리를 단일화하려면, fetchBaseQuery는 “기능보다 구조”를 깔끔하게 만들어주는 선택이었습니다.
프론트엔드 개발자를 위한 SQL 개념 정리
들어가며 프론트엔드 개발자는 일반적으로 API를 통해 데이터를 받아 화면에 표시하는 역할을 수행합니다. 이 과정에서 SQL은 백엔드 개발자나 데이터 엔지니어의 영역으로 여겨지는 경우가 많습니다. 하지만 협업 과정에서 다음과 같은 말을 자주 듣게 됩니다. “그건 WHERE에서 필터링했어요.” “이건 JOIN으로 합쳐야 돼요.” “ORDER BY 순서가 달라서 정렬이 꼬였어요.” 이런 대화를 정확히 이해하려면 SQL의 기본 개념을 알고 있어야 합니다. SQL 문법을 모두 외울 필요는 없지만, 주요 용어를 알고 있으면 API 설계와 데이터 흐름을 훨씬 명확하게 파악할 수 있습니다. 핵심 요약 Table에 대한 이해 WHERE: 필터 조건 → 검색 및 필터 UI와 대응 ORDER BY: 정렬 기준 → 정렬 옵션 구현과 대응 JOIN: 데이터 결합 → 복합 데이터 응답 구조 이해 GROUP BY / HAVING: 통계, 요약 데이터 LIMIT / OFFSET: 페이지네이션 NULL: 값이 존재하지 않음, 예외 처리 필요 트랜잭션: 요청 단위의 일관성 관리 마무리 1. 테이블(Table): 데이터의 구조적 단위 SQL에서 데이터는 테이블(table)에 저장됩니다. 이는 API로 전달되는 JSON 데이터의 근원이기도 합니다. - SQL 용어 의미 프론트엔드에서의 대응 Table 데이터를 저장하는 표 API의 데이터 원본 Column 열, 데이터 속성 JSON의 key Row 행, 실제 데이터 JSON의 객체 한 건 예시 쿼리: SELECT name, email FROM users; API 응답 예시: [ { "name": "배채빵", "email": "chaieun@naver.com" } ] 즉, users 테이블에서 name과 email 열만 선택한 결과가 API로 전달됩니다. 2. WHERE: 데이터 필터링 조건 WHERE 절은 특정 조건에 맞는 데이터만 선택할 때 사용합니다. 프론트엔드의 검색, 필터, 정렬 기능은 대부분 이 절과 대응됩니다. 예시 쿼리: SELECT * FROM users WHERE age >= 25 AND city = 'Yongin'; REST API 요청으로는 다음과 같은 형태와 대응됩니다. GET /users?age_gte=25&city=Yongin 따라서 백엔드가 “WHERE 조건이 빠졌어요”라고 말하면 API에 필터링 로직이 누락되었다는 의미입니다. 3. ORDER BY: 정렬 기준 데이터를 특정 기준으로 정렬할 때 ORDER BY 절을 사용합니다. 프론트엔드에서 정렬 옵션을 구현할 때 백엔드의 이 로직과 대응됩니다. 예시 쿼리: SELECT * FROM posts ORDER BY created_at DESC; 이는 “최신순으로 내림차순 정렬”을 의미합니다. API 요청 예시: GET /posts?sort=created_at&order=desc 4. JOIN: 여러 테이블 결합 JOIN은 두 개 이상의 테이블을 결합해 하나의 결과로 만드는 연산입니다. 백엔드가 “이건 JOIN으로 합쳤어요”라고 말할 때는, 여러 데이터 소스를 하나의 API 응답으로 통합했다는 뜻입니다. 예시 쿼리: SELECT u.name, p.title FROM users u JOIN posts p ON u.id = p.user_id; 결과 데이터 예시: [ { "name": "배채빵", "title": "SQL 배우기" } ] JOIN의 주요 유형은 다음과 같습니다. JOIN 종류 설명 예시 상황 INNER JOIN 두 테이블 모두에 존재하는 데이터만 결합 게시글이 있는 사용자만 조회 LEFT JOIN 왼쪽 테이블의 모든 데이터를 유지 모든 사용자 조회, 게시글이 없으면 NULL JOIN 개념을 이해하면, API 응답 구조가 왜 그렇게 구성되어 있는지 쉽게 파악할 수 있습니다. 5. GROUP BY와 HAVING: 데이터 집계 GROUP BY는 동일한 값을 가진 행들을 묶어 통계나 요약 데이터를 계산할 때 사용합니다. HAVING은 그룹화된 결과에 조건을 적용할 때 사용합니다. 예시 쿼리: SELECT department, AVG(salary) FROM employees GROUP BY department HAVING AVG(salary) >= 3000; 이 쿼리는 부서별 평균 급여가 3000 이상인 부서만 반환합니다. 프론트엔드에서는 주로 차트, 요약 카드, 통계 페이지 등에서 이와 같은 데이터 구조를 다루게 됩니다. 6. LIMIT과 OFFSET: 페이지네이션 SQL에서 LIMIT과 OFFSET은 데이터를 일정 단위로 나누어 가져올 때 사용됩니다. 이는 프론트엔드의 페이지네이션 또는 무한 스크롤 로직과 직접적으로 연결됩니다. 예시 쿼리: SELECT * FROM posts ORDER BY created_at DESC LIMIT 10 OFFSET 20; 이 쿼리는 21번째부터 30번째까지의 데이터를 조회합니다. API에서는 다음과 같이 표현됩니다. GET /posts?page=3&limit=10 7. NULL: 비어 있는 값 SQL의 NULL은 “값이 존재하지 않음”을 의미합니다. 0이나 빈 문자열('')과는 다릅니다. 프론트엔드로 전달될 때는 null 또는 undefined 형태로 나타나므로 UI에서 적절히 처리해주어야 합니다. {user.bonus ?? 0} 이는 bonus가 null이면 0을 표시하는 코드입니다. 8. 트랜잭션(Transaction): 작업 단위 트랜잭션은 여러 SQL 작업을 하나의 논리적 단위로 묶은 것입니다. 하나라도 실패하면 전체가 취소(rollback)되고, 모두 성공하면 최종 반영(commit)됩니다. 예를 들어 송금 시 다음 두 작업이 모두 성공해야 합니다. 송금자 계좌에서 금액 차감 수신자 계좌에 금액 추가 트랜잭션은 데이터 일관성을 유지하기 위한 핵심 개념이며, 프론트엔드에서는 요청 실패 시 UI나 알림 처리 방식에 영향을 줍니다. 9. 마무리 SQL은 단순히 데이터베이스를 조작하기 위한 언어가 아닙니다. 데이터가 어떻게 구성되고, 어떤 과정을 거쳐 프론트로 전달되는지를 이해하게 해주는 언어입니다. 프론트엔드 개발자가 SQL 용어를 이해하면 API 설계 논의를 보다 명확히 이해하고, 데이터 흐름을 논리적으로 파악할 수 있습니다.
리액트로 만든 SW 프로그램을 exe 파일로 배포하자!
팀 내 업무 자동화를 위해 React 기반의 프론트엔드와 FastAPI 기반의 백엔드로 프로그램을 개발했습니다. 개발 환경에서는 npm start로 React 개발 서버를 실행하고, FastAPI 서버를 로컬에서 띄우는 방식으로 사용했지만, 문제는 당장 서버를 할당받아 웹 서비스 형태로 배포할 수 없는 상황이었습니다. 사내 인프라 정책과 승인 절차 때문에 클라우드나 온프레미스 서버에 올리는 것은 단기간에 불가능했기에 사용자 입장에서는 웹 서비스 접속 대신 설치 후 바로 실행할 수 있는 데스크톱 프로그램이 필요했습니다. 즉, React 앱을 exe 파일 형태로 배포해야 했습니다. 1. Node.js 기반 앱을 exe로 만드는 방법은 어떤게 있는지? React로 만든 웹 애플리케이션을 설치형 프로그램(exe) 형태로 배포하기 위해, 먼저 Node.js 기반 앱을 어떻게 exe로 패키징하는지부터 알아보았습니다. 대표적으로는 두 가지 방법이 있는데, 1) pkg 방식 pkg는 Node.js 프로젝트를 단일 실행 파일(.exe)로 패키징해주는 도구입니다. 쉽게 말해, Node.js 런타임과 애플리케이션 코드를 하나의 바이너리로 묶어주는 방식입니다. 장점 결과물이 가볍고 단일 파일로 배포 가능. CLI 도구나 서버 사이드 유틸리티 제작에 적합. 단점 React 같은 UI가 있는 앱은 직접 실행할 수 없음. 브라우저를 강제로 띄워서 UI를 보여주는 식으로 우회해야 해서 UX가 떨어짐. 2) Electron 방식 Electron은 Chromium(브라우저 엔진)과 Node.js 런타임을 함께 패키징하는 프레임워크입니다. 즉, React/Vue 같은 웹 앱을 그대로 데스크톱 앱처럼 실행할 수 있게 해줍니다. 장점 React UI를 그대로 실행 가능 윈도우/맥/리눅스 등 크로스 플랫폼 지원 로컬 파일 시스템, 알림, IPC 통신 등 데스크톱 전용 API 제공 단점 Chromium까지 포함하기 때문에 exe 용량이 커짐 (보통 50~100MB 이상) 정리하자면 pkg는 터미널 기반 유틸리티에는 적합하지만, 데스크톱 앱에는 한계가 있고, Electron은 UI/UX 중심의 데스크톱 앱에 적합합니다. 저희가 만든 프로그램은 단순한 CLI 유틸리티가 아니라, React 기반 UI를 그대로 제공해야 했습니다. 사용자가 버튼을 클릭하면 FastAPI API가 호출되고, 그 결과가 화면에 바로 표시되어야 합니다. 따라서 실행 파일 용량이 다소 커지더라도 사용자 경험을 위해 Electron 기반 exe 제작을 선택했습니다. 3. 환경 설정 및 제작 과정 1) 폴더 구조 프로젝트명/ frontend/ package.json # CRA + Electron 의존성 및 빌드 설정 electron/ main.js # Electron 엔트리 파일 (윈도우 생성, dev/prod 분기) preload.js # 보안 브릿지 (렌더러 <-> 메인 안전한 통신) build/ # CRA 빌드 산출물 release/ # electron-builder 산출물 2) Electron 환경 세팅 npm i -D electron electron-builder concurrently cross-env wait-on electron: 데스크톱 앱 실행 환경 electron-builder: exe 패키징 도구 concurrently: CRA dev 서버 + Electron 동시 실행 wait-on: CRA 서버 준비될 때까지 대기 cross-env: 환경변수 주입 3) main.js 작성 const { app, BrowserWindow, shell } = require('electron'); const path = require('path'); // 개발 모드 여부 const isDev = !app.isPackaged; function createWindow() { const win = new BrowserWindow({ width: 1200, height: 800, webPreferences: { preload: path.join(__dirname, 'preload.js'), // 보안 브릿지 }, }); if (isDev) { win.loadURL(process.env.ELECTRON_START_URL || 'http://localhost:3000'); // 개발 모드 win.webContents.openDevTools(); } else { win.loadFile(path.join(__dirname, '..', 'build', 'index.html')); // 배포 모드 } // 외부 링크는 기본 브라우저에서 열기 win.webContents.setWindowOpenHandler(({ url }) => { shell.openExternal(url); return { action: 'deny' }; }); } // 앱 시작 시 창 생성 app.whenReady().then(createWindow); // macOS 제외, 모든 창 닫히면 종료 app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); Electron 앱의 진입점으로, 창을 생성하고 개발/배포 모드에 따라 React 앱을 불러오는 역할을 하는 파일 외부 링크를 OS 브라우저로만 열어 보안을 지키고, 실행 시 필요한 설정(예: 백엔드 주소)을 주입해주는 역할 4) preload.js 작성 const { contextBridge } = require('electron'); // 렌더러(React)에서 접근 가능한 API 노출 contextBridge.exposeInMainWorld('api', { // main.js에서 주입한 백엔드 주소 반환 getBackendUrl: () => globalThis.__BACKEND_URL__, }); 렌더러(React)와 메인 프로세스(Electron) 사이의 보안 브릿지 역할을 하는 파일 Node API를 직접 노출하지 않고, 백엔드 주소 등 필요한 기능만 제한적으로 제공해 안전하게 통신할 수 있도록 해줌 5) 빌드 스크립트 작성 "scripts": { "dev": "concurrently \\\\"react-scripts start\\\\" \\\\"wait-on <http://localhost:3000> && cross-env ELECTRON_START_URL=http://localhost:3000 electron .\\\\"", "build:react": "react-scripts build", "dist": "npm run build:react && electron-builder" } npm run dev → 개발 환경에서 React + Electron 동시에 실행 npm run dist → React 빌드 후 exe 패키징 6) 빌드 산출물 release/프로젝트명 Setup.exe : 설치형 인스톨러 release/win-unpacked/프로젝트명.exe : 설치 없이 실행 가능한 포터블 버전 3. 결과 이 과정을 거쳐 최종적으로 React 기반 앱을 exe 형태로 배포할 수 있었습니다. 사용자는 더 이상 개발 환경을 직접 세팅할 필요가 없고, 단순히 설치형 인스톨러(Setup.exe)나 포터블 실행 파일(프로젝트명.exe)을 실행하면 바로 UI와 백엔드가 연동된 프로그램을 쓸 수 있습니다. 설치형/포터블 두 가지를 제공하면서, 사용자 환경에 따라 유연하게 선택할 수 있게 된 것도 장점이었습니다. 4. 주요 트러블 슈팅 빌드 후 실행 시 ERR_FILE_NOT_FOUND 오류가 발생했습니다. 원인은 CRA(Create React App) 기본 설정에서 정적 리소스 경로를 절대경로(/static/...)로 지정하기 때문이었는데요, 브라우저 환경에서는 문제가 없지만, Electron exe에서는 리소스를 file://… 경로로 불러오기 때문에 절대경로를 찾지 못했던 것이었습니다. 이를 해결하기 위해 package.json에 "homepage": "./"를 설정했습니다. 이렇게 하면 CRA 빌드 결과가 상대경로(./static/...)로 생성되어 exe 내부에서도 정상적으로 리소스를 로드할 수 있었습니다. 5. 회고 처음 exe 빌드를 시도할 때는 막막함과 두려움이 컸지만, 막상 해보니 새로운 걸 만든다는 설렘과 재미도 있었습니다. Electron 환경 구성, 보안 브릿지(preload.js), 사내 네트워크 정책, 코드 서명 문제 등 예상치 못한 이슈들이 이어졌지만, 하나씩 원인을 찾고 해결하면서 역시 개발은 이런 재미로 한다는 생각이 들었습니다! 이번 과정을 통해 웹 앱을 데스크톱 앱으로 패키징하는 전체 흐름 (React → Electron → exe) 실무 환경에서 흔히 맞닥뜨리는 네트워크·보안·빌드 문제 해결 경험 을 얻게 되었습니다. 추후엔 자동 업데이트나 용량 최적화에도 도전해 보고 싶습니다.
멋사 12기, 프론트 파트장으로 보낸 시간
회사 일 + 외부 사이드 플젝(겸업 아님 절대 절대 나의 유일한 생산적인 취미임)을 병행하며 살다가 오랜만에 회고록에 대한 글을 끄적여 보기 위해 테크 블로그를 다시 방문했다. 열심히 글을 쓰는 것도 좋지만 회사 일에 적응을 더 빨리 하고 싶어서 선택과 집중을 했다. 덕분에 회사에서 잘 지내고 있다고 스스로 느끼게 되었다. 입사 한 달차까지만 해도 내가 잘 적응하고 있는지 인지를 못했다만... 어느순간 확 적응이 된 것 같다. 아마 다른 프로님이랑 새로 TF 개발을 진행하면서 그런 느낌이 든 것 같기도 하다! 역시 협업이란 키워드는 내 인생에서 아주 중요한 요소인 것 같다. 그래서 오늘의 회고는 바로바로 대망의 '본교 멋쟁이 사자처럼 12기 프론트엔드 파트장'을 맡으면서 겪어온 것들에 대해 간 단 하 게 정리해보려고 한다. 시작은 2024년 1월이었다. 처음엔 프론트엔드 운영진으로만 참여할 생각이었지만, 내정되어 있던 파트장이 개인 사정으로 인해 동아리를 탈퇴하게 되었다. 예상치 못한 공석이 생기면서, 결국 누군가는 그 책임을 맡아야 했고 남은 운영진 중 누군가가 나설 수밖에 없는 상황이었다. 그렇게 나는, 프론트엔드 운영진으로 참여하기로 한 순간부터 자연스럽게 책임감을 느끼게 되었고 자발적으로 파트장 역할을 맡기로 결심했다. 겨울 방학 동안 교육 자료를 만들면서 정말 시간과 열정을 쏟아부었다. 사실 나도 웹 개발을 아주 잘 아는 건 아니고, 그저 몇 번 경험해본 정도라 초반엔 꽤 불안했다. ‘내가 혹시 잘못된 정보를 전달하면 어떡하지?’ 하는 걱정에 공부도 더 열심히 하게 됐고, 하나의 개념을 정확히 이해하기 위해 공식 문서를 여러 번 찾아보며 다시 정리하곤 했다. 그런 과정을 반복하면서 나도 조금씩 성장하고 있다는 걸 느낄 수 있었다. 그리고 개강을 하고 두두둥- 동아리 홍보 포스터 등장! 본격적으로 시작하는 기분이 들었다. 2023년엔 거의 방치되다시피 했던 동방을 다시 활성화해보자는 마음에, 동아리원들과 함께 청소도 열심히 했다. 지하 같지 않은 지하 공간이라 상태가 썩 좋진 않았는데, 비 오는 날엔 동방이 물바다가 되는 일도 있었고, 거미줄도 엄청나서 하나하나 정리하고 구석구석 닦아냈다. (본인 방보다 깨끗하게 청소함) 아기사자를 맞이하기 위한 면접 대기실을 준비했다. 프론트엔드, 백엔드, 디자인 파트로 나눠 각각 면접을 따로 진행했는데, 각 지원자의 지원서를 꼼꼼히 읽고 그 내용을 바탕으로 질문지를 직접 만들어갔다. 지원자 한 명 한 명에게 진심으로 다가가고 싶었고, 그만큼 철저하게 준비했던 시간이었다. 비대면 회의를 해요... 11기에 비해 운영진 수도 적었기 때문에 한 명 한 명이 참 소중했었다. 그래도 초반에 운영 관련해선 대표와 부대표가 많은 노력으로 전반적인 관리를 해주어서 나는 세션 준비에 많은 시간을 할애할 수 있었다. 첫 세션 전 리허설! 1차시로 OT는 웹에 대한 기본 상식 (브라우저, http 통신 등)과 Git과 Github에 대한 설명은 내가 진행하고, 2차시엔 다른 프론트 운영진이 html, css, js에 대한 간략한 세션으로 진행을 준비했었다. 대망의 OT... 중앙에서 준비한 오티 영상에 11기 해커톤 영상이 나오는데 내 모습이 깜짝 출연하여 많이 놀랐다 하하 첫 세션의 시작! 멋사 동아리는 전공자뿐 아니라 비전공자, 신입생부터 고학번까지 정말 다양한 배경의 사람들이 모여 있기 때문에, 처음부터 어려운 내용을 다루기보다는 기본부터 탄탄히 다지는 데 집중했다. HTML과 CSS 같이 기초적인 내용도 실제 예제를 통해 쉽게 이해할 수 있도록 구성했고, 처음 프로그래밍을 접하는 사람도 흥미를 느낄 수 있게 설명 방식이나 자료 구성에도 신경을 많이 썼다. 모두가 함께 출발선에 설 수 있도록, 그리고 포기하지 않도록 만드는 것이 가장 중요한 목표였다. 팀 올가니제이션 연결하기! 각자의 PC 환경이 조금씩 달라서 예상치 못한 여러 이슈들이 생기기도 했지만, 그럴 때마다 함께 문제를 공유하고 해결해보려 노력했다. 처음 겪는 오류에 당황하기도 했지만, 직접 해결해보는 과정을 통해 팀원들 모두가 한층 더 성장할 수 있었다. 이렇게 함께 부딪히고 배워가는 경험이 협업의 시작이라고 느꼈다. 사람마다 속도 차이가 있는 건 당연한 일이기 때문에, 세션 중 해결하지 못한 과제나 에러가 남아 있는 경우엔 따로 시간을 마련해 함께 해결해보고자 했다. ‘같이의 가치’를 실천하고 싶었고, 혼자 뒤처졌다고 느끼지 않도록 최대한 옆에서 도와주려 했다. 함께 고민하고 같이 해결해보는 그 과정이 결국 모두의 성장을 만드는 힘이라고 믿는다! 그리고… 기억에 남는 과제 제출 코드도 있었다. 누구인지는 말 못 하지만, 리액트 과제였는데 제출된 건 HTML 파일 하나였다. 순간 어라..? 싶었지만, 그만큼 React라는 라이브러리 자체가 낯설고 어렵게 느껴질 수도 있겠다는 생각이 들었다. 어디서부터 막혔는지, 어떤 개념에서 어려움을 겪었는지를 다시 점검해볼 필요성을 느꼈고, 앞으로 과제를 낼 때도 사전 설명이나 가이드가 더 필요하겠다는 걸 배웠다. 팀원들에게 코드 피드백도 하게 되었다. 물론 코드에 정답은 없지만… 어느 정도 “비슷한 답”은 있는 것 같기도 하다. 같은 기능을 구현하더라도 더 읽기 쉬운 코드, 유지보수가 쉬운 구조가 있다는 걸 배워가면서 서로의 코드를 비교하고 개선점을 함께 찾아보는 과정이 꽤 의미 있었다. 아이디어톤! 내가 낸 아이디어가 채택되어 직접 서비스 기획을 맡게 되었다. 3학년 1학기와 2학기, 두 학기 모두 21학점에 동시에 5개의 팀 프로젝트 진행, 주말 마감 알바까지 병행하면서 정말 정신없이 바쁜 시기를 보냈다. 그러다 보니 어느 순간, 대외적인 ‘나’와 내면의 ‘나’ 사이의 경계가 점점 흐려지고 있다는 걸 느꼈다. 겉으로 보이는 성과나 평가에 따라 내 자존감이 요동쳤고, 일이 조금만 틀어져도 자책하며 스스로를 깎아내리곤 했다. 이런 악순환을 막기 위해 ‘내면의 나와 외면의 나를 분리해 바라보면 어떨까?’라는 생각이 들었고, 그 고민에서 출발한 아이디어를 아이디어톤에서 제안하게 되었다. 아주 귀엽고 센스 넘치는 디자인을 만들어준 예진이에게 무한 감사! 팀원들과 함께 나의 아이디어를 단순히 시각적으로 풀어낸 것뿐만 아니라, 배경지식과 기획 의도를 함께 조사하고, 발표용 자료까지 정리해준 덕분에 아이디어가 더 설득력 있고 완성도 있게 다듬어질 수 있었다. 비록 해당 서비스는 아쉽게도 2등에 머물렀지만… 내 마음 속에선 단연 1등이었다.ㅎㅎ 처음엔 어려워하는 팀원들을 위해 내가 정리해둔 Git 명령어 컨닝페이퍼를 공유해줬다! 그땐 나도 참고하면서 썼었는데 지금은 주요 명령어는 거의 다 외우고, git stash 같은 꿀팁 명령어들도 상황에 맞게 잘 활용하고 있다.ㅎㅎ 하나씩 익숙해지면서 Git이 점점 편해졌다는 걸 실감하게 됐다. 그런데 실무에 나와보니 GUI를 쓰는 사람들이 꽤 많은 것 같았다. 그치만 난 여전히 CLI파 입니다만... 어느게 더 좋은지 다른 개발자들의 의견이 궁금하기도 하다! 누군가의 문제를 함께 해결해줄 때 가장 큰 보람을 느꼈다. 그럴 때마다 ‘아, 이런 순간이 내가 제일 즐거워하는 일이구나’라는 걸 실감하게 된다. 이런 경험을 통해, 나중에 기회가 된다면 교육 분야에서도 진로를 고민해보는 것도 재미있겠다는 생각이 들었다. 사람과 함께 성장하는 일, 꽤 나랑 잘 맞는 것 같다! 이걸 과제로 냈는데요... 돌아온 과제가 이랬다면 제 심정은 어땠을까요....?(질끈) 그래서 따로 준비한 과제 예시 코드…!!! 정말 감을 못 잡고 힘들어하던 비전공자, 1학년 아기사자 친구에게만 조심스럽게 몰래 보내줬다. 다행스럽게도 이 코드 덕분에 흐름을 이해할 수 있었다고 해서 꽤 뿌듯했다.ㅎㅎ 이렇게 작은 도움 하나가 누군가에겐 큰 전환점이 될 수 있다는 걸 다시 한 번 느꼈다. 세션을 마무리하며 프론트엔드 팀의 토이 프로젝트 발표를 진행했다. 1팀은 우피로 제작된 멋사 공식 페이지를 React로 직접 구현했고, 2팀은 배달의민족 홈페이지를 클론 코딩하여 완성도 높게 만들어냈다. 짧은 시간 동안 각자 맡은 역할을 해내고 결과물까지 만들어낸 걸 보며 정말 뿌듯했다. 과제로 끙끙 거렸던 사람들 맞나요...? 감동의 눈물이 줄줄 세션 마지막 날엔 해커톤 대비를 위해 인텔리제이 환경 세팅을 도와줬다. 혹시 모를 백엔드의 배포 이슈로 프론트 개발자 로컬에서 서버를 돌릴지도 모르니,,, (실제 경험담) 그렇게 시작된 멋사 해커톤 기간! 우리 팀은 4명 모두 인턴을 병행하고 있어서 시간적으로 굉장히 촉박한 상황이었다. 게다가 팀 내에 디자이너도, PM도 없었기 때문에 초반엔 정말 오합지졸 그 자체였다. 누가 무엇을 맡아야 할지, 어디서부터 시작해야 할지도 막막했지만 그 와중에도 하나씩 역할을 나누고 조금씩 방향을 잡아가며 해커톤을 시작하게 됐다. 디자이너 친구에게 브랜딩과 로고를 부탁했더니 이렇게 다양한 모양과 색감으로 로고를 만들어줬다. 갓예진 그녀는 대체... 그렇게 랜딩 페이지도 만들어졌다. 디자이너 두 명의 도움을 받아가며 어찌저찌 하나씩 완성해 나갔다. 처음엔 막막했지만, 센스 있는 팀원들 덕에 역할을 나누고 조금씩 채워가다 보니 어느새 형태를 갖추게 됐다. 주말마다 카페에 출근 도장 찍듯 나갔다. 물론 힘들긴 했지만… 어쩌겠어, 해야지!! 해야 할 건 많고 시간은 부족했지만, 그래도 하나씩 해내는 맛이 있었다. 프론트 개발 사항과 진행을 한 눈에 보기 위해 노션으로 정리했다. 아주 열심히 했구만~ 9to6 인턴 생활과 해커톤의 병행... 그리고 계속 오르는 커밋수 팀원(덩미니)은 결국 실소하게 되었다ㅋ 그래도 팀의 분위기는 매우 매우 좋았다. 프로젝트를 진행하면서 팀원들끼리 친해진게 컸던 것 같다!! 해커톤 디데이~! 풀페이지로 개발한 랜딩 페이지와 찰칵- 프롬프트를 공유하는~~ 서비스 소개 페이지! 화려한 UI를 개발하는게 내 주특기가 되었다 하하 PPT 잘 만들었나요..? 해당 서비스에 새로운 기능을 추가해 교내 학술제에 출품했다. 짧은 해커톤으로 끝내기엔 아쉬운 프로젝트였기에, 기획을 보완하고 기능을 확장하면서 더 발전된 형태로 완성해볼 수 있었다. 그 결과, 교내 학술제에서 2등 우수상을 받게 되었다~ 짧은 시간 동안 고생한 보람이 있었고, 직접 만든 서비스를 인정받았다는 점에서 더 뜻깊었다. (아… 끝내주는 꿈을 꾸었던 걸까?) 2024년은 정말 다양한 도전으로 가득했던 한 해였다. 멋사 활동뿐 아니라 벤처스타트업 아카데미 수료, 단기 인턴, 경희대 연합 해커톤 참가, 처음 도전해본 리액트 네이티브 앱 개발까지… 그 어느 해보다 바쁘게, 그리고 치열하게 보냈다. 하반기엔 본격적으로 취업 준비도 시작했다. 여러 곳에 서류를 넣고, 합격과 탈락을 반복하면서 내가 부족한 점은 무엇인지, 어떤 부분에 강점이 있는지를 많이 되돌아보게 됐다. 카카오모빌리티 서류 합격 후 코딩테스트까지 진행했지만 아쉽게도 2차에서 탈락했고, 가보고 싶던 게임회사 역시 서류는 통과했지만 최종까지는 닿지 못했다. 그리고 지금 다니는 회사에 붙기 직전, 한 회사의 면접에서 만난 기술 이사님과 현직 개발자 두 분의 피드백이 생각보다 꽤 오래 마음에 남았고, 나중에 따로 회고록으로 꼭 써보려고 한다. (언젠가… 정말로…) 멋사 12기 활동에는 아쉬움도 남지만, 함께했던 좋은 사람들은 여전히 곁에 있고 어느새 모두 졸업을 하고, 사회인이 되어가는 모습을 보며 그래도 다행이라는 마음이 들었다. 모두가 각자의 자리에서 잘 살아가길. 그리고 나 역시, 계속해서 나답게 성장하길. 다들 무사히, 건강하게 지내길 바라며 -
30개의 상태가 달린 하나의 컴포넌트를 마주친 적 있으신가요?
30개의 상태관리가 달려있는 하나의 컴포넌트를 마주하신 적 있으신가요? 최근 제가 맡은 업무 중 하나는, 파일 변환 기능을 담당하는 컴포넌트의 기능 추가 구조 개선 < 이를 중점적으로 글을 쓸 거예요. 배포 및 테스트 이 세 가지를 모두 책임지는 일이었어요. 처음에는 “기존 코드에 기능 몇 개만 추가하면 되겠지”라는 마음으로 가볍게 시작했지만, 실제로 마주한 코드는 예상과는 조금 달랐어요. return 문만 보면 현기증이… 이 컴포넌트에는 useState가 약 30개가 나열돼 있었고, UI 구성은 Tailwind CSS 라이브러리를 사용한 형태로 되어 있었어요. Tailwind는 스타일을 클래스 단위로 태그에 바로 작성하는 방식이라, 작은 컴포넌트에선 굉장히 생산적일 수 있지만, return문 안에 여러 레이아웃과 조건부 렌더링이 얽혀 있을 경우 한눈에 파악하기가 쉽지 않은 느낌이었어요. 예시로 들면 ... return ( <div className="flex flex-col gap-4 w-full bg-white p-6 rounded-xl shadow-md"> {isUploading && <p className="text-sm text-blue-500">업로드 중...</p>} <input type="file" onChange={handleFileChange} className="border border-gray-300 rounded-md p-2" /> {error && <p className="text-sm text-red-500">{error}</p>} {/* ... 이런 코드가 계속 반복 */} </div> ); 그리고 이 컴포넌트 안에… const [file, setFile] = useState(null); const [fileName, setFileName] = useState(''); const [isUploading, setIsUploading] = useState(false); const [error, setError] = useState(''); // ... 그 외 20여개가 더 이런 useState가 무더기로 선언되어 있었어요. 그 상태들은 대부분 자식 컴포넌트에도 props로 전달되고 있었고요. 이쯤 되면 감이 오겠죠? 상태 관리의 복잡성이 문제의 핵심이었어요. 이 문제를 방치하면 나중에는 프롭스 드릴링(props drilling) 즉, 상위 컴포넌트에서 선언한 상태를 하위 컴포넌트로 계속해서 깊게 전달해야 하는 구조로 인해 컴포넌트 간 결합도가 높아지고, 컴포넌트 재사용성은 떨어지며, 버그 추적이나 유지보수가 매우 어려워지는 상황에 빠지게 될 수 있어요. 내가 아닌 다른 사람이 만든 코드를 고쳐나가는 것은 처음입니다만 ... 코드를 보며 저는 고민에 빠졌어요. 작동이 되는 코드를 괜히 고쳤다가... 안 되면 어떡하지... 해당 소스는 이미 프로젝트에서 사용 중인 기능이기도 했고, 무엇보다 "내가 감히 다른 개발자의 코드를 손봐도 괜찮을까?" 라는 막연한 부담감도 있었어요. 내가 쓴 코드라면 언제든 뜯어고치고, 고장 나도 다시 고치면 되지만, 남이 만든 코드에 손을 댄다는 건 '이 구조가 틀렸다'고 판단해야 하는 일이기도 하니까요. 그래서 처음엔 파일을 열어두고도 며칠을 머뭇거렸어요. "정말 바꿔야만 하나?", "정 내가 힘들어도 그냥 참고 쓰는 게 낫지 않을까?" 하는 생각을 계속 반복했어요. "이대로 두면, 나도 나중에 발목 잡힌다" 그렇게 며칠을 고민하다가 결국 리팩토링을 결심한 데에는, 단순히 지금 눈앞에 있는 문제 때문만은 아니었어요. 1. 언젠가는 이 컴포넌트를 남에게 설명해야 한다 앞으로 제가 다른 프로젝트로 넘어가거나, 혹은 누군가 이 기능을 인수받게 되는 순간이 분명 올 텐데, 어떤 기능이 어떤 흐름으로 동작하는지,이 상태는 왜 여기 있고, 왜 이렇게 전달되고 있는지 설명하려면 그 누구보다 제가 이 구조를 잘 이해하고 있어야 하고, 결국엔 “남이 보기 쉬운 코드”로 바꿔놔야 내가 설명할 수 있겠다는 생각이 들었어요. 2. 기능을 추가하려면 지금부터 정리돼 있어야 한다 그리고 무엇보다 이 컴포넌트는 단발성 기능이 아니었어요. 앞으로도 계속 이 코드를 재활용하고 유용하게 쓰기 위해서는 구조가 명확하고 깔끔해야 한다는 생각했어요. 그런데 지금처럼 useState가 30개씩 흩어져 있고, 컴포넌트 구조도 무겁게 얽혀 있는 상태라면 기능 하나 추가할 때마다 다른 기능이 망가질 수도 있다는 불안함이 생겼어요. 즉, 지금 손대지 않으면 나중에는 더 손도 못 댈 정도로 커지겠다는 위기감이 들었어요. 그리고 제가 항상 생각해왔지만, 저는 개발을 할 때 ‘불가능이란 없다’라고 믿고 시작하면 사고가 확 열리면서 신기하게도 길이 보이기 시작하더라고요. 이번에도 마찬가지였어요. 어떻게든 해내야겠다는 생각뿐이었습니다. “일단 나눠보자. 나누다 보면 흐름이 보이겠지.” 그렇게 상태를 하나하나 분류하고, 컴포넌트의 역할을 정리하면서 점점 더 코드가 이해되기 시작했고, 마침내 내가 책임질 수 있는 구조로 리팩토링을 마칠 수 있었어요. 어떻게 나눴는가: 리팩토링의 방향을 잡기 위해 생각한 것들 막연히 “나눠야겠다”는 생각만으로는 리팩토링이 되지 않을거에요. 그래서 논리적으로, 효율성 있게 생각해보려고 노력했어요. 무엇을 기준으로 나눌 것인가, 그리고 나누면 어떤 이점이 생길 것인가에 대한 기준을 만들었고, 다음과 같은 질문을 스스로에게 던지며 방향을 잡아갔어요. ✅ 1. 기능이 분리되는가? → API 호출 로직부터 훅으로 추출 가장 먼저 눈에 들어온 건 API 호출 로직이었어요. 컴포넌트 내부에 주기능 함수들이 모두 들어있었고, 동작의 흐름은 명확하게 나뉘어 있었지만, UI와 무관한 로직까지 컴포넌트 안에 섞여 있는 상태였어요. "이건 굳이 컴포넌트 안에 있을 필요가 있을까?" 그래서 구조를 정리하기 시작했어요. 컴포넌트 내부의 주요 기능 함수들은 역할(기능) 단위로 나누어 커스텀 훅으로 분리했고, API 요청과 관련된 로직은 api 폴더 안으로 이동시켜 apiClient.ts에서 공통 axios 인스턴스를 관리하고, 기능별로 각각의 API 요청은 파일 하나씩 나누어 정리했어요. 이렇게 나누고 나니, 테스트가 훨씬 쉬워졌고 비즈니스 로직과 UI 로직이 자연스럽게 분리되어 코드가 눈에 훨씬 잘 들어오고 유지보수도 편한 구조가 되었어요. ✅ 2. UI의 책임이 명확한가? → 화면 영역 기준으로 컴포넌트 분리 다음으로 살펴본 건 화면 구조였어요. 초기에 기능 단위로 로직을 분리하면서, 자연스럽게 각 UI가 담당하는 역할도 명확하게 드러났어요. 그래서 "시각적으로 구분되는 영역 = 하나의 컴포넌트" 라는 기준을 세우고 리팩토링을 진행했어요. 이 기준에 따라 UI를 나눠보니 각 컴포넌트가 어떤 일을 담당하는지 한눈에 파악할 수 있었고, 더 이상 거대한 컴포넌트를 해석하느라 스트레스를 받을 일도 없어졌어요. ✅ 3. 상태 관리, 제대로 하고 있는가? 마지막으로 남은 숙제는 상태 관리였어요. 처음 코드를 봤을 때, 최상위 컴포넌트가 너무 많은 useState를 가지고 있었고, 그걸 하위 컴포넌트로 계속 props로 넘기는 구조였어요. 문제는, 일부 상태는 특정 UI에서만 쓰이는데도 전체 컴포넌트가 그 상태의 영향을 받아 불필요하게 리렌더링되고 있었던 거예요. 그래서 아래 기준을 세워 상태의 위치를 정리했어요. 1. 여러 컴포넌트에서 공유되는 상태 → 최상위로 올리기 2. 특정 기능에서만 쓰이는 상태 → 그 기능 내부 컴포넌트로 내리기 3. 재사용 가능한 전역 상태 → 전역 상태 관리 도구 Recoil로 분리 이렇게 상태의 위치를 정리함으로써, 비효율적인 렌더링을 줄이고 코드의 흐름을 명확히 했으며, 결과적으로 새로운 기능을 추가하거나 버그를 수정할 때도 훨씬 더 효율적으로 작업할 수 있게 되었어요. 이번 작업을 통해 느낀 건, 리팩토링은 단순히 코드를 보기 좋게 정리하는 일이 아니라는 점이에요. 진짜 중요한 건 왜 그렇게 나눴는지, 그 구조가 어떤 문제를 해결했는지를 논리적으로 설명할 수 있는 코드로 만드는 거였어요. 그래야 나중에 다른 누군가가 이 코드를 보더라도 쉽게 이해할 수 있고, 무엇보다 내가 다시 봤을 때도 낯설지 않고 자신 있게 수정할 수 있다고 느꼈어요. 그래서 리팩토링을 진행하면서 계속 스스로에게 질문을 던졌어요. "이 로직은 여기 있어야 할까?" "이 상태는 왜 이 위치에 있지?" 이런 고민을 하면서 기준을 세우고, 그 기준에 맞춰 구조를 하나씩 정리해 나갔어요. 그 과정을 통해 결국 내가 설명할 수 있는 코드, 두렵지 않고 자신 있게 다룰 수 있는 코드를 만들 수 있었고, 이번 경험이 단순한 코드 정리를 넘어 개발자로서 한 걸음 더 성장하는 계기가 되었다고 느꼈어요. 아마 다음에도 이런 상황에 마주하면 자신감 있게 맞설 수 있겠단 생각을 했습니다! 다음 글에는 이 세가지 과정을 구체적으로 설명하는 글을 써보도록 할게요. 그 전에 저 야근 좀 그만하고 싶어요................ (누구보다 야근을 즐기는 광기를 보이며) 살려줘요