낭 만 당

신입 개발자의 입사 4주차 회고 본문

Programming Record

신입 개발자의 입사 4주차 회고

낭만알파카 2026. 1. 30. 17:42

 

입사 4주차(1월 26일~30일)를 회고합니다.

이번 주는 29일 회사 자체 상품인 중국인들을 위한 의료 관광 플랫폼  한성미 서비스 운영 시작을 앞두고 정말 바쁜 한 주였습니다. QA를 진행하며 시술 안내 세부페이지 퍼블리싱 작업을 다수 진행했고, 내부 UX 기능 개선도 병행했습니다. 특히 이번 주에는 소켓 통신을 활용한 실시간 동기화백엔드 API 개발까지 경험하며 풀스택에 한 걸음 더 다가간 한 주였습니다.


주요 작업 요약

이번 주는 한성미 서비스 런칭 준비, 기술적 복잡도가 높은 기능 개발, 내부 시스템 개선 세 가지 축으로 업무가 진행되었습니다.

서비스 런칭 준비 (한성미 의료관광)

  • 시술 안내 세부페이지 퍼블리싱 (한글/중문) 다수
    • 모발이식 절개/비절개, 눈밑 지방 재배치, 이중턱 지방흡입, 레이저제모 등
  • 자주 묻는 질문 섹션 구축[1]
  • 환율 정보 표시 (위엔화)[2]
  • 이용약관/개인정보처리방침 모달
  • 의사 소개 영역 퍼블리싱 (20개 게시물)

기술적 복잡도가 높은 기능

  • 쿠폰 관리 시스템 (프론트엔드 + 백엔드)[3]
  • 메시지 센터 필터/검색 (프론트엔드 + 백엔드)[4]
  • 실시간 예약 정보 동기화 (소켓 통신)[5]

내부 UX 개선

  • 위챗 상담 정보 복사 기능[6]
  • 웹 회원 엑셀 다운로드[7]
  • 메시지 센터 날짜 필터 개선[8]

기술적 깨달음

1. 소켓 통신과 실시간 동기화의 복잡성

이번 주 가장 기술적으로 흥미로웠던 작업은 판매점 예약관리 Drawer에서 실시간 업데이트 감지[5] 기능이었습니다.

문제 상황

여러 관리자가 동시에 같은 예약을 조회하고 수정할 때, A가 수정한 내용을 B가 모르고 덮어쓰는 문제가 발생할 수 있습니다. 특히 Drawer(사이드 패널)를 열어둔 상태에서는 다른 곳의 변경사항을 알 수 없었죠.

해결 방법: 소켓 기반 실시간 감지

서버에서 onTourReserveUpdate 소켓 이벤트를 브로드캐스트하면, 모든 연결된 클라이언트가 이를 수신합니다. 하지만 여기서 까다로운 점이 있었습니다.

 

내가 수정한 경우 vs 타인이 수정한 경우를 구분해야 한다는 것.

 

[예시코드]

// 의사 코드
const lastSaveTime = useRef<number>(0);

// 저장 시
const doSaveAction = async () => {
  await axios.put('/api/reserve', data);
  lastSaveTime.current = Date.now(); // 저장 시점 기록
};

// 소켓 수신 시
socket.on('onTourReserveUpdate', (data) => {
  const isMyUpdate = data.oxuid === profile.browserUid;
  const isRecent = Date.now() - lastSaveTime.current < 2000;
  
  if (isMyUpdate && isRecent) {
    // 내가 방금 저장한 거니까 무시
    console.log('[Socket] Skip fetch...');
    return;
  }
  
  // 타인이 수정했거나, 내가 다른 탭에서 수정한 경우
  if (confirm('다른곳에서 예약정보가 수정되었습니다. 최신정보로 조회하시겠습니까?')) {
    refreshData(); // 최신 데이터 다시 불러오기
  }
});

핵심 로직

  1. 내가 저장한 직후 (2초 이내): 내 browserUid가 소켓 데이터와 일치하고, 저장한 지 2초 이내면 무시
  2. 타인이 수정한 경우: Confirm 다이얼로그로 사용자에게 알리고 선택 받기
  3. 내가 다른 탭에서 수정한 경우: browserUid는 같지만 lastSaveTime이 갱신되지 않았으므로 자동으로 최신 데이터 반영

배운 점

  • browserUid의 활용: 각 브라우저 세션마다 고유 ID를 부여하면 "누가 수정했는지" 구분 가능
  • 시간 기반 필터링: lastSaveTime을 활용해 "내가 방금 저장한 액션"인지 판단
  • 사용자 경험 고려: 내 액션은 조용히 무시하고, 타인의 액션만 알림
  • 멀티 탭 처리: 같은 사용자가 여러 탭을 열었을 때도 자연스럽게 동기화

이 작업을 통해 실시간 협업 시스템에서 충돌을 방지하는 패턴을 배웠습니다. 단순히 소켓으로 데이터를 받는 것을 넘어서, "누구의 액션인지", "언제 발생한 액션인지"를 구분하는 로직이 핵심이었습니다.

 

2. 프론트엔드에서 백엔드까지: 쿠폰 관리 시스템

쿠폰 관리 시스템[3] 작업은 이번 주 가장 복잡하고 완성도가 높았던 작업이었습니다. 프론트엔드뿐만 아니라 백엔드 API도 직접 구현했습니다.

요구사항

  1. 검색 기능: 전체/ID/쿠폰코드/쿠폰명/할인가격/주문최소금액/사용방식/메모/생성일/상태 중 선택 검색
  2. 쿠폰 회수 기능: 체크박스로 여러 쿠폰 선택 후 일괄 회수 (사용된 쿠폰은 제외)
  3. 쿠폰 삭제 기능: 회수된 쿠폰만 삭제 가능
  4. 상태 관리: 발급 → 사용됨(100) / 회수됨(99) → 삭제

프론트엔드 구현

// 필터 드롭다운 + 검색바
<Select value={searchField} onChange={setSearchField}>
  <MenuItem value="all">전체</MenuItem>
  <MenuItem value="id">ID</MenuItem>
  <MenuItem value="code">쿠폰코드</MenuItem>
  // ... 기타 필드
</Select>
<TextField 
  placeholder={getPlaceholder(searchField)} // 선택한 필드에 따라 동적 변경
  onKeyDown={handleEnter}
/>

동적 Placeholder가 인상적이었습니다. 사용자가 "쿠폰코드"를 선택하면 "쿠폰코드를 입력하세요", "생성일"을 선택하면 "YYYY-MM-DD 형식으로 입력하세요" 같은 식으로 힌트가 바뀝니다.

백엔드 구현

백엔드에서는 동적 쿼리를 구현해야 했습니다.

// 의사 코드
const searchCoupons = async (field: string, keyword: string) => {
  let query = 'SELECT * FROM coupons WHERE 1=1';
  const params: any[] = [];
  
  if (field === 'all') {
    // 모든 필드에서 검색
    query += ` AND (code LIKE ? OR name LIKE ? OR memo LIKE ?)`;
    params.push(`%${keyword}%`, `%${keyword}%`, `%${keyword}%`);
  } else if (field === 'status') {
    // 상태는 정확히 일치
    query += ` AND status = ?`;
    params.push(keyword);
  } else {
    // 특정 필드에서만 검색
    query += ` AND ${field} LIKE ?`;
    params.push(`%${keyword}%`);
  }
  
  return await db.query(query, params);
};

배운 점

  • 동적 쿼리 작성: 사용자가 선택한 필드에 따라 쿼리를 동적으로 생성
  • SQL Injection 방지: Prepared Statement(Parameterized Query) 사용
  • 상태 머신 설계: 발급 → 회수 → 삭제 순서를 강제해서 데이터 무결성 보장
  • UI 피드백: 회수 모드에서는 사용된 쿠폰을 비활성화해서 실수 방지
  • 확인 다이얼로그: 삭제 같은 위험한 액션은 반드시 확인 절차 추가

처음으로 프론트엔드와 백엔드를 모두 구현해보니, API 설계가 프론트엔드 구현에 얼마나 큰 영향을 주는지 체감했습니다. 좋은 API는 프론트엔드 코드를 간결하게 만들고, 나쁜 API는 프론트엔드에서 복잡한 가공 로직을 요구합니다.

3. 메시지 센터: 복잡한 필터링과 검색

메시지 센터[4][8] 작업도 백엔드 API 구현이 포함된 작업이었습니다.

요구사항

  • 카카오 알림톡, SMS/MMS, 이메일 3가지 탭
  • 날짜 필터: 전체/오늘/이번주/이번달/지난 7일/지난 30일
  • 검색 필드: 수신자명, 전화번호, 이메일
  • 수신 실패 토글: 실패한 메시지만 필터링

기술적 도전

날짜 필터와 검색, 토글이 조합되어야 한다는 점이 까다로웠습니다.

// 의사 코드
const filters = {
  dateRange: { start: '2026-01-01', end: '2026-01-30' },
  searchField: 'receiver',
  searchKeyword: '홍길동',
  failedOnly: true
};

// 백엔드에서 조합된 쿼리 생성
SELECT * FROM messages 
WHERE sent_date BETWEEN ? AND ?
AND receiver LIKE ?
AND status = 'failed';

특히 날짜 필터 버튼의 상태 관리가 흥미로웠습니다.

  • 필터 버튼 클릭: 해당 버튼 활성화 표시
  • 달력으로 직접 수정: 모든 버튼 비활성화 (커스텀 날짜이므로)
  • 스위치 토글: 즉시 검색 실행 (별도 검색 버튼 클릭 불필요)

배운 점

  • 복합 필터 설계: 여러 조건이 AND로 결합되는 구조
  • UI 상태 동기화: 날짜를 바꾸면 필터 버튼 상태도 함께 업데이트
  • 즉시 반영 vs 버튼 클릭: 토글은 즉시 반영, 텍스트 검색은 Enter 키나 버튼 클릭
  • 기본값 설정: "전체" 날짜가 기본값이지만, 성능을 위해 실제로는 합리적인 범위(예: 최근 3개월)를 쿼리

4. 다국어 환경에서의 조건부 렌더링

위엔화 환율 표시[2] 작업은 간단해 보였지만 의외로 고려할 점이 많았습니다.

요구사항

  • 중국어 언어 설정인 경우에만 위엔화 표시
  • KRW 위에 표시
  • 원화와 동일하게 끝에 ~ (물결) 표기
  • color: #666, font-weight: 500
// 의사 코드
const PriceDisplay = ({ priceKRW }) => {
  const { language } = useLanguage();
  const priceYuan = convertToYuan(priceKRW);
  
  return (
    <div>
      {language === 'zh' && (
        <div style= color: '#666', fontWeight: 500 >
          ¥{priceYuan.toLocaleString()}~
        </div>
      )}
      <div>
        ₩{priceKRW.toLocaleString()}~
      </div>
    </div>
  );
};

기술적 고려사항

  1. 환율 계산: 실시간 환율 API를 사용할지, 고정 환율을 사용할지
  2. 표시 순서: 위엔화를 위에 둘지 아래 둘지 (중국 사용자 입장에서는 위엔화가 더 친숙)
  3. 스타일 일관성: 원화와 같은 포맷팅(천 단위 콤마, 물결 표시)
  4. 조건부 렌더링: 한국어 사용자에게는 불필요한 정보이므로 렌더링 자체를 안 함

배운 점

  • 현지화(Localization)는 단순 번역이 아님: 가격 표시 방식, 통화 단위, 문화적 맥락까지 고려
  • 조건부 렌더링 최적화: {condition && <Component />} 패턴으로 불필요한 DOM 생성 방지
  • 사용자 입장에서 생각하기: 중국 고객은 위엔화로 환산해서 "대략 얼마인지" 파악하고 싶어 함

5. 동적 콘텐츠 렌더링: 자주 묻는 질문

자주 묻는 질문 영역[1] 구축은 관리자 편의성과 사용자 경험을 모두 고려해야 했습니다.

구조

관리자가 게시글 ID만 입력하면, 해당 게시글을 하단 고정 영역에 자동으로 렌더링합니다.

// 의사 코드
const FAQSection = ({ pinnedPostIds }) => {
  const [faqs, setFaqs] = useState([]);
  
  useEffect(() => {
    // 관리자가 입력한 ID 목록으로 게시글 조회
    const fetchFAQs = async () => {
      const posts = await Promise.all(
        pinnedPostIds.map(id => fetchPost(id))
      );
      setFaqs(posts);
    };
    fetchFAQs();
  }, [pinnedPostIds]);
  
  return (
    <div className="faq-section">
      {faqs.map(post => (
        <FAQItem key={post.id} title={post.title} content={post.content} />
      ))}
    </div>
  );
};

배운 점

  • 관리자 친화적 설계: 새 FAQ를 추가하려면 게시글을 작성하고 ID만 입력하면 됨 (별도 UI 개발 불필요)
  • 동적 로딩: ID 목록이 바뀌면 자동으로 해당 게시글들을 불러와서 렌더링
  • 확장성: 나중에 FAQ가 100개가 되어도 시스템 변경 없이 ID만 추가하면 됨
  • 재사용성: 일반 게시글 시스템을 활용하므로 에디터, 이미지 업로드 등의 기능을 그대로 사용 가능

이 패턴은 **"기존 시스템을 영리하게 재활용"**하는 좋은 예시였습니다. 새로운 시스템을 만드는 것보다, 기존 게시판 시스템을 활용해서 특정 게시글을 특별하게 표시하는 방식이 훨씬 효율적이었습니다.

6. 퍼블리싱 작업에서 배운 일관성의 중요성

이번 주에 시술 안내 세부페이지를 정말 많이 만들었습니다. 모발이식 절개/비절개, 눈밑 지방 재배치, 이중턱 지방흡입, 레이저제모 등등...

처음 3~4개를 만들 때까지는 각 페이지마다 조금씩 다른 스타일을 적용했는데, 나중에 보니 일관성이 없어서 어색했습니다.

개선 과정

  1. 컴포넌트 분리: 반복되는 섹션(시술 과정, 주의사항 등)을 컴포넌트로 분리
  2. 스타일 시스템 정리: 제목 크기, 간격, 색상 등을 변수로 관리
  3. 레이아웃 템플릿: 기본 레이아웃 구조를 템플릿으로 만들고 내용만 바꾸기
// Before: 각 페이지마다 하드코딩
<h2 style= fontSize: '28px', marginBottom: '16px' >시술 과정</h2>

// After: 공통 컴포넌트 사용
<SectionTitle>시술 과정</SectionTitle>

배운 점

  • 반복 작업에서 패턴 찾기: 비슷한 작업을 3번 이상 하면 공통 패턴을 추출해야 함
  • 디자인 시스템의 필요성: 제목, 본문, 버튼 등의 스타일을 미리 정의해두면 일관성 유지가 쉬움
  • 템플릿 기반 개발: 구조는 같고 내용만 다른 페이지는 템플릿으로 만들어서 재사용

특히 한글/중문 페이지를 동시에 만들 때, 컴포넌트 구조가 같으면 번역 작업이 훨씬 수월했습니다.


아쉬웠던 점

1. 소켓 에러 처리 부족

소켓 통신 구현은 성공했지만, 연결이 끊어졌을 때의 처리가 부족했습니다. 네트워크가 불안정하거나 서버 재시작 시 자동으로 재연결하는 로직이 필요할 것 같습니다.

// 아직 구현 안 한 부분
socket.on('disconnect', () => {
  // 재연결 시도?
  // 사용자에게 알림?
});

2. 백엔드 테스트 부족

백엔드 API를 직접 구현했지만, 단위 테스트를 작성하지 못했습니다. 프론트에서 수동으로 테스트했지만, 엣지 케이스(빈 문자열, SQL Injection 시도 등)를 체계적으로 테스트하지 못한 게 아쉽습니다.

3. 퍼블리싱 초반의 시행착오

퍼블리싱 작업 초반에 컴포넌트화와 일관성을 고려하지 않고 진행해서, 나중에 리팩토링 시간이 추가로 들었습니다. 처음부터 패턴을 고민했으면 더 효율적이었을 것 같습니다.


느낀 점

풀스택 개발의 매력

이번 주에 프론트엔드와 백엔드를 모두 구현해보면서, 전체 데이터 흐름을 이해하니 훨씬 더 좋은 설계를 할 수 있다는 걸 느꼈습니다.

프론트만 할 때는 "API가 이 데이터를 주면 좋겠는데..."라고 바라기만 했다면, 이제는 "이렇게 API를 설계하면 프론트에서 쓰기 편할 거야"라고 직접 만들 수 있게 되었습니다.

물론 아직 백엔드 지식이 부족하지만, 전체 시스템을 이해하려는 노력이 결국 더 나은 개발자가 되는 길이라고 생각합니다.

실시간 기능의 복잡성

소켓 통신을 구현하면서, 실시간 기능이 단순히 "빠른 것"이 아니라 "동기화 문제"를 해결하는 것임을 배웠습니다.

  • 누가 수정했는지
  • 언제 수정했는지
  • 내가 보고 있는 데이터가 최신인지
  • 충돌이 발생하면 어떻게 해결할지

이런 문제들을 하나씩 해결해가는 과정이 어려웠지만, 매우 재미있었습니다.

서비스 런칭의 긴장감

29일 한성미 서비스 운영 시작을 앞두고, 실제 사용자가 쓸 서비스를 만든다는 긴장감을 느꼈습니다.

퍼블리싱 하나하나가 "중국 고객이 실제로 볼 페이지"이고, 환율 계산이 틀리면 "실제 예약에 영향을 줄 수 있다"는 생각에 더 신중해졌습니다.

테스트할 때도 "내가 고객이라면 이 페이지를 보고 예약하고 싶을까?"를 계속 생각했습니다. 단순히 "작동하는 코드"가 아니라 "사람들이 실제로 사용할 서비스"를 만든다는 것의 무게감을 느낀 한 주였습니다.

반복 작업에서 효율을 찾는 법

퍼블리싱 작업을 많이 하면서, 반복 작업을 효율화하는 패턴을 배웠습니다.

  1. 처음 1~2개: 천천히 만들면서 패턴 파악
  2. 3~4개부터: 반복되는 부분을 컴포넌트로 분리
  3. 5개 이상: 템플릿 기반으로 빠르게 작업

이런 과정을 거치면서, "일을 열심히 하는 것"과 "일을 똑똑하게 하는 것"의 차이를 배웠습니다.


다음 주 목표

  1. 소켓 통신 고도화: 재연결 로직, 에러 처리 학습
  2. 백엔드 테스트: Jest나 다른 테스팅 프레임워크로 API 테스트 작성법 공부
  3. 디자인 시스템 제안: 팀 차원에서 공통 컴포넌트/스타일 정리 필요성 논의
  4. 한성미 서비스 피드백 수집: 실제 운영 후 개선사항 파악

입사 4주차를 마무리하며, 이제는 단순히 "할당받은 작업을 완료하는 것"을 넘어서 "더 나은 구조는 무엇일까", "사용자 경험을 어떻게 개선할까"를 고민하게 되었습니다.

소켓 통신, 백엔드 개발, 서비스 런칭... 하나하나가 새로운 도전이었지만, 그만큼 많이 배우고 성장한 한 주였습니다. 다음 주에도 계속해서 기술적으로 깊이 있게 공부하며 발전하는 개발자가 되고 싶습니다!