Curl Up Black Cat

😎 서론

안녕하세요! 오늘은 React Hook Form 과 useRef 의 조합을 통해 특정 input 요소에 직접 전근하는 방법에 대해 공유하고자 합니다.
최근 게시판 프로젝트에서 게시글을 열 때 댓글 작성자 필드에 자동으로 포커스를 주고 싶었는데 React Hook Form 도입 시에 이를 구현하는데 어려움을 겪었습니다. 이에 대한 내용과 해결방법을 공유합니다.


🤔 문제상황 왜 ref={commentAuthorInputRef} 로는 동작하지 않을까?

React Hook Form 을 사용할 때, 주요 기능 중 하나는 register 함수입니다. 이 함수는 input 필드를 관리하고, 유효성 검사나 폼 제출 등의 다양한 기능을 제공합니다. 제가 처음에 사용한 방식은 아래와 같았습니다.

const commentAuthorInputRef = useRef(null)

...

useEffect(() => {
    commentAuthorInputRef.current.focus();
}, []);

...

<input ref={commentAuthorInputRef} ... />

그런데, 위와 같은 방식으로 구현하면 React Hook Form 의 register 와 ref 와 동시에 연결되지 않아, 입력 값이 undefined 로 반환되는 문제가 발생했습니다.
 
register 함수는 input 필드를 등록하면서 해당  필드의 참조 ref 를 내부적으로 관리합니다. 그렇기 때문에, 외부에서 별도의 ref 를 연결할 때는  register 와의 연동이 중첩될 수 있습니다. 이 때문에, 내부적으로 폼 값의 상태 관리나 유효성 검사와 같은 기능들이 제대로 동작하지 않게 됩니다.


🌟 해경 방법: ref 를 함수로 정의하기

React Hook Form 의 register 함수는 ref 와 함께 객체를 반환합니다. 이를 입력 요소에 연결하면서 동시에 외부 ref 에도 연결해야 했기 때문에, 함수를 사용하여 이를 해결해야 합니다.

 <input
  {...commentRegister('author', { 
  	required: '댓글 작성자는 필수 입력입니다.' 
  })}
  ref={(e) => {
    commentRegister('author').ref(e); // react-hook-form 을 위한 ref
    commentAuthorInputRef.current = e; // 외부 ref
  }}
  ...
/>

위 코드에서 중요한 점은 ref 함수가 두 가지 연결 작업을 동시에 수행한다는 것입니다.
1. React Hook Form 의 ref 에 요소(HTMLInputElement)를 연결
2. 외부 ref에도 동일한 요소(HTMLInputElement)를 연결
 
이를 통해 React Hook Form 의 기능을 그대로 활용하면서도, 외부에서도 input 요소에 접근할 수 있게되었습니다.🤭


🎬  결론

React Hook Form 은 매우 편리한 도구이지만, 때때로 다른 React 기능과의 조합이 필요할 때가 있습니다. 오늘 소개한 방식은 그러한 경우에도 도움이 될 것입니다. 특히, 외부 ref와 함께 React Hook Form 을 사용하고자 할 때 이 방식이 큰 도움이 될 것입니다. 
 
그럼 오늘도 즐겁게 코딩하세요! 😁
 


😎 서론

오늘은 리액트에서 동적 라우팅을 사용하여 페이지 모드를 어떻게 활용하는지 작성합니다. 게시판 화면을 만들어 보면서 등록, 수정, 상세의 화면을 단 하나의 페이지에서 관리하는 것이 목표였습니다. 이를 리액트로 개발하면서 페이지 간의 이동 데이터의 활용을 어떻게 해야 효과적으로 구현할 있을까? 고민하다가 동적 라우팅 활용을 알게 되었습니다.


😃 페이지 모드의 정의

먼저, AppTypes 파일에서 페이지의 모드를 관리합니다.

const AppTypes = {
  PageMode: {
    view: 'view',
    add: 'add',
    edit: 'edit',
  },
  ...
}

페이지의 모드는 view, add, edit 가지로 구분됩니다. 위 코드처럼 App 에서 활용된느 각 모드 타입을 별도의 파일로 분리하여 관리하면 프로젝트의 구조가 더욱 명확해지고 유지 보수가 쉬워집니다.


🌟 리액트 라우터의 활용

리액트에서 페이지간의 이동을 관리하는 가장 효과적인 방법 중 하나는 리액트 라우터를 활용하는 것입니다. 특히 동적 라우팅동일한 UI 구조를 가지면서 다양한 데이터를 보여주는 페이지를 구현할 때 매우 유용합니다. 페이지 모드를 활용해 라우터에서는 다음과 같이 경로컴포넌트를 연결해 주었는데요..

import { Navigate, Route, Routes } from 'react-router-dom'

const App = () => {
  return (
    <Routes>
      <Route path="/board/list" element={<LayoutWithComponent layout={EmptyLayout} component={Board} />} />
      <Route path="/board/view/:id" element={<LayoutWithComponent layout={EmptyLayout} component={BoardView} />} />
      <Route path="/board/:mode" element={<LayoutWithComponent layout={EmptyLayout} component={BoardView} />} />
      ...
    </Routes>
  )
}

/board/:mode 경로와 같이 동적 라우트 방식으로 게시판의 다양한 모드를 효율적으로 사용하고 있습니다.

이렇게 재사용성을 높이면 개발 과정에서의 복잡도를 줄일수 있습니다.

 

동적 라우팅으로 페이지를 넘기는 이벤트 핸들러 코드 예시입니다.

const handleAddClick = useCallback(() => {
  navigateTo(`/board/${AppTypes.PageMode.add}`)
}, [navigateTo])

const handleRowClick = useCallback((id) => {
  navigateTo(`/board/view/${id}`)
}, [navigateTo])

다양한 상황에 따라 유연하게 URL을 구성하여 해당 URL 에 맞는 페이지로 라우팅 할 수 있습니다.


📑 페이지 모드 관리

View 에서는 게시글의 상세 내용을 보여주며, 각각의 모드에 따라 적절한 액션 버튼들을 제공합니다. useParams 사용하여 라우트에서 필요한 매개변수를 가져와 상태를 지정하였습니다.

const BorderView = () => {
  const { mode, id } = useParams() // mode: AppTypes.PageMode
  const [currentMode, setCurrentMode] = useState(mode || AppTypes.PageMode.view)
...
}

이렇게 할 경우 조건부 렌더링을 활용하여 현재 모드 따라 다른 UI 요소 보여줄 있습니다.

{currentMode === AppTypes.PageMode.view && (
  <>
    <button type="button" className="list-btn" onClick={handleListClick}>목록으로</button>
    <button type="button" className="edit-btn" onClick={handleEditClick}>편집</button>
  </>
)}

{currentMode !== AppTypes.PageMode.view && (
  ...
)}

외에도 모드에 따라 다른 이벤트 핸들러를 설정하거나 데이터를 요청하는 등의 동작을 분기처리할 있습니다.

const onSubmit = useCallback(async (data) => {
  const payload = {
    ...data
  }
  
  try {
    if (currentMode === AppTypes.PageMode.add) {
      await BoardService.create(payload)
      navigateTo('/board')
    }
  
    if (currentMode === AppTypes.PageMode.edit) {
      await BoardService.update(id, payload)
      navigateBack()
    }
  } catch (err) {
    handleError(err.response)
  }
}, [currentMode, id, navigateTo, navigateBack, handleError])

🔄 페이지 모드 변경하기

useState 에 선언한 setCurrentMode 함수를 통해 페이지의 동작 모드를 변경하여 이를 통해 사용자의 액션(: 편집 버튼 클릭) 따라 동작 모드를 변경하고, 그에 따른 UI 갱신할 있습니다.

const handleEditClick = useCallback((e) => {
  e.preventDefault()
  setCurrentMode(AppTypes.PageMode.edit)
}, [])

🎬 결론

리엑트 라우터의 동적 라우팅을 활용하면, 사용자의 요청에 따라 페이지의 내용과 상태를 유연하게 변경할 수 있습니다. 페이지 모드의 관리와 라우팅을 최적화하는 방법을 살펴봤는데, 리엑트 프로젝트에서 이 기법을 적용하면 사용자 경험은 물론, 개발자의 작업 효율도 상당히 증진될 것이라 생각합니다. 🙂


😎 서론 

안녕하세요 오늘은 Spring에서 자주 마주치는 문제 중 하나인 API 잘못된 메시지 요청에 대한 이야기를 해보려고 합니다. 그리고 이를 어떻게 처리했는지 작성해 볼게요.

 

게시판 프로젝트를 만들어 보면서 백엔드에서 클라이언트로부터 API 요청을 받게 되면, 요청의 JSON 포맷에 오류가 있는 경우가 있었습니다. 그 중에서도 알 수 없는 필드를 전달 받았을 때 이 예외가 발생합니다. 이 문제를 어떻게 해결 할 수 있을까 고민을 했고..


🤔 HttpMessageNotReadableException 처리

Spring 에서 제공하는 @ExceptionHandler 를 사용하여 HttpMessageNotReadableException 을 처리하면서, 그 원인이 되는 cause 를 통해 더 구체적인 예외 상황을 파악하는 방법을 알게되었습니다.

@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity handleJsonParseException(HttpMessageNotReadableException ex) {
    final Throwable cause = ex.getCause();
    ...
}

 

이 때, 발견한 것이 UnrecognizedPropertyException 이라는 것입니다. 이 예외는 JSON알 수 없는 필드 때문에 발생하므로, 해당 필드의 이름을 얻어와 클라이언트에게 더 구체적인 오류 메시지를 전달하고자 했습니다.

if (cause instanceof UnrecognizedPropertyException) {
    final UnrecognizedPropertyException unrecognizedPropertyException = (UnrecognizedPropertyException) cause;
    final String fieldName = unrecognizedPropertyException.getPropertyName();
    ...
}

 

그리고 이렇게 얻어온 필드 이름을 ErrorCode.UNKNOWN_FIELD 와 함께 반환하여 클라이언트에게 해당 필드가 문제라는 것을 응답하였습니다. 이렇게 처리하면, 클라이언트는 어떤 필드 때문에 요청이 실패했는지 쉽게 파악할 수 있습니다.😀

 

전체 소스를 보면 아래와 같이 @ExceptionHandler 를 구축해보았습니다.

@ExceptionHandler(value = {HttpMessageNotReadableException.class})
public ResponseEntity handleJsonParseException(HttpMessageNotReadableException ex) {
    final Throwable cause = ex.getCause();

    if (cause instanceof UnrecognizedPropertyException) {
        final UnrecognizedPropertyException unrecognizedPropertyException = (UnrecognizedPropertyException) cause;
        final String fieldName = unrecognizedPropertyException.getPropertyName();
        final String errorMessage = String.format(ErrorCode.UNKNOWN_FIELD.getMessage() + " : '%s'", fieldName);
        final ResponseModel responseModel = ResponseModel.failure(ErrorCode.UNKNOWN_FIELD, errorMessage);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseModel);
    }

    // 기본 메시지 처리
    final ResponseModel responseModel = ResponseModel.failure(ErrorCode.INVALID_JSON, ex.getMessage());
    return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseModel);
}

 

그 밖에 JSON 문법 오류 / 타입 불일치 / JSON 구조의 불일치 / 데이터 바인딩 오류가 있는 경우는 기본 메시지 처리로 응답하는 INVALID_JSON 으로 응답하도록 구현하였습니다.


🤠 Response 응답 메시지

구현된 핸들러의 응답메시지는 한번 살펴보겠습니다.🙂

 

잘못된 필드 데이터 전달이 되었을 경우..

UnrecognizedPropertyException 예외

 

JSON 문법오류 등 기본 메시지 전달이 되었을 경우..

그 밖의 HttpMessageNotReadableException 예외


🥸 결론

예외 처리는 단순히 오류를  포착하는 것을 넘어 중요한 역할을 하는거 같습니다. 이를 통해 사용자와의 원활한 소통과 협업이 가능하며, 이로 인해 보다 나은 서비스를 제공할 수 있게 됩니다. 게시판 프로젝트를 만들어 보면서 이에 대한 연구와 고민을 지속적으로 해 나갈 계획입니다.🙂

 


😎 서론

프로젝트를 진행하다보면 여러 곳에서 반복적으로 라우팅 관련 기능을 구현하게 되는 경우가 많습니다. 이를 해결하기 위해 React Router 의 useNavigate 를 활용하여 보다 편리하게 사용할 수 있는 custom hook 을 만들어 보았습니다.

 

import { useNavigate } from 'react-router-dom'

const useAppNavigate = () => {
  const navigate = useNavigate()

  const navigateBack = () => {
    navigate(-1)
  }

  const navigateTo = (path) => {
    navigate(path)
  }

  return { navigateBack, navigateTo }
}

export default useAppNavigate

여기서 useAppNavigate 라는 custom hook 은 두가지 기능을 제공합니다.

 

1. navigateBack: 현재 페이지에서 이전 페이지로 돌아가는 기능

2. navigateTo: 원하는 경로로 이동하는 기능

 

이제 이 custom hook을 어떻게 사용하는지 간략하게 살펴보겠습니다.

 

🤠 navigateBack 활용

때론 사용자에게 '뒤로가기' 기능을 제공하고 싶을 때가 있습니다. 이때 navigateBack 함수를 사용하면 간단하게 구현할 수 있습니다.

const { navigateBack } = useAppNavigate();

// '뒤로가기' 버튼이 클릭되었을 때 실행
<button onClick={navigateBack}>뒤로 가기</button>

 

🥸 navigateTo 활용

특정 경로로 이동하고자 할 때 navigateTo 함수를 사용합니다.

const { navigateTo } = useAppNavigate();

// '/profile' 경로로 이동하고자 할 때
<button onClick={() => navigateTo('/profile')}>프로필 보기</button>

요약하면 React Router 의 useNavigate 는 굉장히 유용한 기능을 제공하지만, 특정한 사용 상황에 따라 조금 더 확장된 기능이 필요할 때가 있습니다. 이럴 때 위와 같은 custom hook 을 활용하면 보다 편리하게 라우팅 관리를 할 수 있습니다.

 

🤔 왜 Custom Hooks 을 만들었는가?

useNavigate 와 같은 기본 Hooks 가 있다면, 왜 추가적으로 custom Hooks 를 만들었는지 궁금할지도 모릅니다. 이에 대해서 간단하게 설명드리겠습니다.

1. 코드 간결화: 반복되는 라우팅 코드를 간단하게 만들 수 있습니다.

2. 명확성: 함수의 이름을 통해 용도를 바로 알 수 있게 됩니다. 예를 들면, navigateBack 은 바로 뒤로 가는 기능임을 알 수 있습니다.

3. 유연성: 나중에 라우팅 관련 로직을 변경할 때, 한 곳에서만 수정하면 되므로 관리가 쉽습니다.

4. 재사용: 여러 컴포넌트에서 같은 로직을 쉽게 재사용할 수 있습니다.

 

🤭 결론

Custom Hooks 는 프로젝트의 효율성과 가독성을 높이는 핵심 도구입니다. 이를 통해 반복되는 코드를 줄이며, 라우팅과 같은 기능을 더 명확하고 직관적으로 표현할 수 있게 되었습니다. 이는 코드의 관리와 수정을 쉽게 하며, 다른 개발자들이 코드를 이해하고 활용하는 데에도 큰 도움을 줍니다.


😎 서론

안녕하세요! 오늘은 Spring Boot 에서 API 응답과 예외 처리 모델을 구축해봤던 내용을 공유해 보려고 합니다. 코드들도 하나하나 분석해 볼께요. 스타트~!

 


1. ResponseModel - 응답 모델

먼저, 모든 API 응답에 공통적으로 사용될 ResponseModel 을 살펴봅니다.

@Getter
@Setter
@ToString
@AllArgsConstructor(staticName = "of")
public class ResponseModel<T> {
    private boolean success;
    private T data;
    private ErrorModel error;

    public static <T> ResponseModel<T> of(boolean success, T data) {
        return ResponseModel.of(success, data, null);
    }

    public static ResponseModel of(boolean success, ErrorCode code) {
        return ResponseModel.of(success, null, ErrorModel.of(code.name(), code.getMessage(), null));
    }

    public static ResponseModel of(boolean success, ErrorCode code, Exception ex) {
        return ResponseModel.of(success, null, code, ex);
    }

    public static <T> ResponseModel<T> of(boolean success, T data, ErrorCode code, Exception ex) {
        final ErrorModel error = (code != null) ? ErrorModel.of(code, ex) : null;
        return ResponseModel.of(success, data, error);
    }
}

주요 구성 요소:

1. success: 요청 처리의 성공 여부를 나타냅니다.

2. data: 요청에 대한 결과 데이터를 포함합니다.

3. error: 발생한 오류에 대한 정보를 포함하는 ErrorModel 객체 입니다.

 

ResponseModel 에는 여러 static 생성 메서드가 포함되어 있습니다. 이 메서드들을 통해 성공 또는 오류 상황에 따라 쉽게 응답 객체를 구성할 수 있습니다.

 

2. ErrorModel - 에러 모델

ErrorModel 은 오류에 대한 정보를 표준화하여 전달하기 위한 모델입니다.

 

package com.board.backend.model;

import com.board.backend.common.ErrorCode;
import com.board.backend.common.utils.ObjectUtil;
import com.board.backend.common.utils.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@AllArgsConstructor(staticName = "of")
public class ErrorModel {
    private String code;
    private String message;
    private ErrorData data;

    @Getter
    @Setter
    @ToString
    @AllArgsConstructor(staticName = "of")
    public static class ErrorData {
        private String exceptionMessage;
        private String stackTrace;
    }

    public static ErrorModel of(ErrorCode code, Exception ex) {
        final String exceptionMessage = ObjectUtil.nonEmpty(ex) ? ex.getMessage() : null;
        final String stackTrace = ObjectUtil.nonEmpty(ex) ? getStackTraceAsString(ex) : null;
        final ErrorData data = (StringUtil.nonEmpty(exceptionMessage) || StringUtil.nonEmpty(stackTrace)) ? ErrorData.of(exceptionMessage, stackTrace) : null;
        return ErrorModel.of(code.name(), code.getMessage(), data);
    }

    private static String getStackTraceAsString(Exception ex) {
        final StringBuilder sb = new StringBuilder();
        for (StackTraceElement element : ex.getStackTrace()) {
            sb.append(element.toString());
            sb.append("\n");
        }
        return sb.toString();
    }
}

주요 구성 요소:

1. code: 오류 코드를 나타냅니다.

2. message: 오류 메시지를 나타냅니다.

3. data: 오류 발생 시의 추가 정보를 나타내는 ErrorData 객체입니다.

 

ErrorData 클래스는 발생한 예외의 메시지와 스택 트레이스 정보를 포함합니다. 이를 통해 오류의 원인을 더욱 자세히 파악할 수 있습니다.(스택 트레이스란? 프로그램에서 예외가 발생했을 때 해당 예외의 발생 경로를 추적하는 정보입니다. 이는 메서드 호출 순서와 라인 번호를 포함하여 오류가 발생한 위치와 그 원인을 확인할 수 있게 도와줍니다.)

 

3. ErrorCode - 에러 코드

@Getter
@AllArgsConstructor
public enum ErrorCode {
    SERVER_ERROR("서버 에러"),
    ARGUMENT_TYPE_MISMATCH("잘못된 파라미터타입이 전달되었습니다."),
    BOARD_NOT_FOUND("글이 존재하지 않습니다");
    ...
    private String message;
}

각 오류 유형에 대한 메시지를 함께 제공하여 사용자에게 의미 있는 오류 메시지를 전달 합니다. enum 타입으로 코드와 메시지를 함께 관리하였습니다.

 

4. ExceptionAdvice - 전역 예외 처리기

Spring Boot 에서 예외 처리의 중요성을 강조하며, 이를 위해 @ControllerAdvice@ExceptionHandler 를 통해 효과적인 전역 예외 처리 방식을 제공합니다.

@ControllerAdvice
public class ExceptionAdvice {
    @ExceptionHandler(value = {Exception.class})
    public ResponseEntity handleException(Exception ex, WebRequest request) {
        final ResponseModel responseModel = ResponseModel.of(false, ErrorCode.SERVER_ERROR, ex);
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(responseModel);
    }

    @ExceptionHandler(value = {MethodArgumentTypeMismatchException.class})
    public ResponseEntity handleTypeException(Exception ex) {
        final ResponseModel<Object> responseModel = ResponseModel.of(false, ErrorCode.ARGUMENT_TYPE_MISMATCH,  ex);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(responseModel);
    }
    ...
}

 

1) @ControllerAdvice 의 역할

@ControllerAdvice 는 Spring 에서 모든 컨트롤러에서 발생할 수 있는 예외들을 한곳에서 관리하고 처리하기 위해 도입된 어노테이션입니다. 이를 통해 코드의 중복을 줄이고, 일관된 예외 처리 로직을 구성할 수 도와줍니다. 

 

2) @ExceptionHandler 

@ExceptionHandler 는 특정 예외를 처리할 메서드를 지정하기 위한 Spring MVC 의 어노테이션입니다. 그 자체로도 유용하지만, @ControllerAdvice와 결합하여 사용할 경우, 애플리케이션 전체의 예외를 중앙에서 효율적으로 관리할 수 있게 됩니다.

 

예시:

// 단일 지정
@ExceptionHandler({CustomException.class})

// 다중 지정
@ExceptionHandler({CustomException1.class, CustomException2.class, CustomException3.class})

이렇게 설정하면, 위에서 지정된 예외들이 발생했을 때 동일한 방식으로 처리됩니다.

 

3) 확장성

@ControllerAdvice 의 장점중 하나는 그 확장성입니다. 새로운 예외를 추가하거나 예외 처리 방식을 수정해야 할 때, @ExceptionHandler 를 적절한 메서드에 추가하거나 수정함으로써 중앙에서 예외 처리 로직을 쉽게 관리하고 확장할 수 있습니다.


🤠 결론

Spring Boot 환경에서 나름대로 생각해서 구현했던 응답과 예외 처리 방법에 대해 알아보았습니다. 일관된 응답과 정확한 예외 처리는 API의 신뢰성을 높이는 핵심 요소로, 이를 통해 사용자와의 신뢰 관계를 구축하고, 개발팀 간의 협업 효율을 높일 수 있습니다. 🙂


😎 서론

안녕하세요! 오늘은 스프링 부트에서 JSON 데이터를 최적화하는 간단한 방법에 대해 알려드리려 합니다. 서비스에서 작은 최적화도 사용자 경험에 변화를 가져올 있기에, 이를 활용하면 많은 이점을 얻을 있습니다.

 


🤔 왜 null 값을 제거해야 하나요?

JSON 응답 데이터에서 null 값은 다음과 같은 문제점을 발생시킬 수 있습니다.

  • 데이터크기: 불필요한 null 값은 전송되는 데이터의 크기를 불필요하게 증가시킵니다.
  • 클라이언트 처리: 클라이언트에서는 null 값에 대한 추가적인 처리가 필요하게 됩니다.

 

Before

{
    "id": 14,
    "title": "제목",
    "content": "내용",
    "writer": "홍길동",
    "created_id": null
}

🥸 Jackson 라이브러리로 Null 값 제거하기

Spring Boot에서는 Jackson 라이브러리를 활용하여 JSON 응답 데이터를 효과적으로 관리할 있습니다. 아래의 설정을 사용하면, null 값을 가진 필드는 JSON 응답에서 자동으로 제외됩니다.

 

package com.board.backend.config;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfig {

    @Bean
    public ObjectMapper objectMapper() {
        final ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        return objectMapper;
    }
}

 

적용 후의 결과 입니다.

 

After

{
    "id": 14,
    "title": "제목",
    "content": "내용",
    "writer": "홍길동"
}

🤠 마치며

JSON 내의 null 값을 제거하는 것은 간단한 절차처럼 보일 수 있지만, 이러한 작은 최적화가 서비스의 응답 시간 개선과 클라이언트의 처리 부담 감소에 큰 도움을 줍니다. 특히, 대용량 데이터를 다루는 서비스나 리소스가 제한된 모바일 환경에서는 이런 최적화가 더욱 중요합니다.

 

데이터를 깔끔하게 관리하고, 사용자에게 필요한 정보만을 효율적으로 전달하는 것은 좋은 서비스의 기본입니다. 글을 통해 소개된 방법이 여러분의 서비스에 적용되어, 사용자 경험을 단계 향상시키는 도움이 되기를 바랍니다.

 


😎 서론

안녕하세요, 인더제이 입니다.

 

오늘은 MyBatis XML Mapper에서 Java 내부 클래스 참조시 발생하는 문제와 그에 대한 해결 방법을 알려드리려 합니다. 개인적으로 문제로 인해 상당한 시간을 헤맸기에 경험을 여러분과 공유하고 싶습니다.

 

🥲 문제 상황

먼저, 아래는 Java 내부 클래스를 포함한 BoardDto 모델입니다.

package com.board.backend.model;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BoardDto {

    @Getter
    @Setter
    public static class Response {
        private long id;
        private String title;
        private String content;
        private String writer;
        private long view_count;
        private Long created_id;
        private LocalDateTime created_dt;
        private LocalDateTime modified_dt;
    }
}

모델을 MyBatis XML Mapper resultType으로 설정하려고 했을 , 내부 클래스 Response 참조해야 했습니다. 처음 작성한 코드는 아래와 같습니다.

<select id="findById" resultType="com.board.backend.model.BoardDto.Response">
    SELECT *
    FROM board
    WHERE id = #{id}
</select>

하지만 위와 같이 설정했을 , 연결이 제대로 이루어지지 않았고, 다음과 같은 에러가 발생했습니다.

Error creating bean with name 'boardMapper' defined in file  ...
Cannot resolve reference to bean 'sqlSessionTemplate' while setting bean property 'sqlSessionTemplate'

🤔 원인 

sqlSessionTemplate 연결 에러의 원인은 다양합니다: 설정 파일 누락, 설정 파일 위치 불일치, 의존성 문제, 데이터 소스 연결 문제, 빈 이름 불일치, 생성자/세터 주입 문제 등. 이 모든 가능성을 확인했지만, 문제의 원인을 찾지 못하고 있었습니다.

그러던 , Java에서 내부 클래스를 참조할 $ 기호로 연결된다는 것을 알게 되었습니다.


🙂 해결방법

Mapper XML에서 resultType 올바르게 설정하기 위해선 $ 기호를 사용해야 합니다.

<select id="findById" resultType="com.board.backend.model.BoardDto$Response">
    SELECT *
    FROM board
    WHERE id = #{id}
</select>

핵심은 BoardDto.Response 대신 BoardDto$Response 사용하는 것입니다.

 

결론적으로, MyBatis에서 Java 내부 클래스를 참조할 때는 $ 기호 사용해야 합니다. 저는  작은 부분 때문에 많은 시간을 낭비하였는데 이 글을 통해 같은 문제를 겪는 분들에게 도움이 되기를 바랍니다. 😌

Java Development Kit (JDK)의 여러 버전 간에는 다양한 기능, 개선 사항 및 변경 사항이 있습니다. JDK 8, 11, 17 간의 주요 차이점을 요약하면 다음과 같습니다.

 

JDK 8

· 람다 표현식(Lambda Expressionis): 코드를 간결하게 만들어 주며, 특히 컬렉션 라이브러리와 잘 작동합니다.

· 스트림 API(Stream API): 새로운 추상화를 통해 데이터 처리 작업을 간결하게 만들어 줍니다.

· 날짜와 시간 API(Date and Time API): 기존의 java.util.Date 와 java.util.Calender 를 대체하는 새로운 API 입니다.

· Default Methods: 인터페이스에 메서드 구현을 포함할 수 있게 되었습니다.

· Nashorn JavaScript Engine: Java 코드 내에서 JavaScript를 실행할 수 있게 합니다.

 

JDK 11

· LTS(Long-Term Support) Release: 이 버전은 오랜 기간 동안 지원됩니다.

· var 키워드: 지역 변수의 타입 추론을 가능하게 합니다. 이를 통해 코드의 중복성을 줄일 수 있습니다.

· String API의 개선: 새로운 메서드들(예: isBlank(), lines(), repeat(), strip())이 추가되었습니다.

· HTTP 클라이언트(HTTP Client): java.net.http 패키지가 추가되어 HTTP/2, 웹소켓 등의 기능을 지원합니다.

· Java EE 및 CORBA 모듈 제거: Java EE와 CORBA 관련 API 및 도구가 JDK에서 제거되었습니다.

 

JDK 17

· LTS(Long-Term Support) Release: JDK 11 이후의 다음 LTS 버전입니다.

· Sealed Classes: 클래스를 seal하여 특정 자식 클래스만 상복받을 수 있도록 제한할 수 있습니다.

· Pattern Matching for Switch(Preview): switch 문을 개선하여 코드를 더 간결하게 만듭니다.

· JEP 356: Enhanced Pseudo-Random Number Generators: 새로운 인터페이스 및 구현을 통해 난수 생성을 개선합니다.

· JEP 382: New macOS Rendering Pipeline: macOS에서의 그래픽 렌더링 성능을 개선합니다.

· JEP 411: Deprecate the Security Manager for Removal: Java의 보안 관리자 기능을 비추천 상태로 만듭니다.

 

이외에도 각 버전마다 수많은 기능, 개선 사항, 버그 수정, 성능 향상이 포함되어 있습니다. 각 버전의 JEP(JDK Enhancement-Proposal) 목록을 확인하면 해당 버전에서 어떤 변화가 있었는지 더운 자세히 알 수 있습니다.

 

더프레이 앨범커버

 
 

 

 
 
이번에 포스팅할 노래는 더프레이(The Fray)의 Naver Say Never 입니다.
 
처음엔 멜로디가 좋아서 즐겨 들었는데.. 가사를 이해하고 들으니 감정도 느껴져서 더 좋았습니다. 
 
떠나려고 하는 사람을 불잡으려는 것 같은 애절함이 있는 노래 같습니다.
 
밴드 보컬인 아이작 슬레이드(Isaac Slade) 의 목소리가 정말 좋아요.
 
노래와 목소리가 정말 잘 어울리네요 🙂
 
좋은 노래 감상 해보세요! 🎧


Never Say Never - The Fray
 
Some things we don't talk about
우리가 애기하지 않는게, 몇 가지 있지
Rather do without
그냥 애기하지 않고
and just hold the smile
그냥 미소를 짓기만 해
Falling in and out of love
사랑에 빠졌다가 말았다가 하며
Ashamed and proud of,
부끄럽다가도 자랑스럽고
together all the while
그러는 내내 우린 함께있지
You can never say never
'절대'라고 절대 말하지마
While we don't know when
우리가 언제인지 모를때
But time and time again
때때로
Younger now than we were before
더 철이 없어지기도 해
 
Don't let me go
날 놓지마
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
 
Picture, you're the queen of everything
상상해봐. 너는 모든 것의 여왕이야
Far as the eye can see
멀어져 볼 수 있을 때까지
under your command
네게 복종할거야
I will be your guardian when all is crumbling
모든게 무너질때도 난 너의 수호자가 될게
I'll steady your hand
네 손을 잡아줄게
You can never say never
'절대'라고 절대 말하지마
While we don't know when
우리가 언제인지 모를때
But time, time and time again
때때로
Younger now than we were before
더 철이 없어지기도 해
 
Don't let me go
날 놓지마
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
 
We're pulling apart
우린 헤어지고
and coming Together again and again
다시 만나는 걸 계속 반복하고 있어
We're growing apart,
우린 멀어지지만
but we pull it together
서로 가까워지지
Pull it together, together again
다시 서로 가까워지는걸
 
Don't let me go
날 놓지마
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
Don't let me go
 

이번 포스팅은 "리액트를 다루는 기술" 서적을 학습하며 내용을 정리하였습니다.

왜 리액트인가?

최근 몇 년간 전 세계 개발자는 자바스크립트에 뜨럽게 열광하고 있습니다. 자바스크립트는 현재 웹 애플리케이션에서 가장 핵심적인 역할을 하고 있습니다. 자바스크립트 언어만으로 데스크톱 애플리케이션, 모바일 애플리케이션 등 규모가 큰 애플리케이션을 만들 수 있는 시대입니다. 대규모 애플리케이션 중 프런트엔드 사이드에서 돌아가는 애플리케이션 구조를 관리하려면.. 어떻게 해야 할까요?

이에 페이스북 개발팀은 아이디어를 고안해 냈습니다.

 

어떤 데이터가 변할 때마다 어떤 변화를 줄지 고민하는 것이 아니라 그냥 기존 뷰를 날려 버리고 처음부터 새로 렌더링 한다.

 

이렇게 하게되면 애플리케이션 구조가 매우 간단해지고, 작성해야 할 코드양이 많이 줄어 들며, 더 이상 어떻게 변화를 줄지 신경 쓸 필요가 없고, 그저 뷰가 어떻게 생길지 선언만 하면 되며, 데이터 변화가 있으면 기존에 있던 것을 버리고 새로 렌더링 되게 됩니다.

 

페이스북 개발 팀은 이런 방식을 채택하여 최대한 성능을 아끼고 편안한 사용자 경험(user experience)을 제공하면서 구현하고자 개발한 것이 리액트(React) 입니다.

 

리액트의 이해

이미지출처: reactjs

리액트는 자바스크립트 라이브러리로서 사용자 인터페이스를 만드는 데 사용합니다. 관리 구조가 MVC, MVW 등인 프레임워크와 달리 오직 V(View)만 신경 쓰는 라이브러리 입니다.

 

리액트에서는 특정 부분이 어떻게 생길지 정하는 선언체가 있는데 이를 컴포넌트(component) 라고 합니다. 컴포넌트는 재사용이 가능한 API 로 수많은 기능들을 내장하고 있으며 컴포넌트 하나에서 컴포넌트의 생김새와 작동 방식을 정의하여 사용하게 됩니다.

 

또한 사용자 화면에 뷰를 보여 주는 것을 렌더링이라고 합니다.

리액트 라이브러리는 뷰를 어떻게 렌더링하길래 데이터가 변할 때마다 새롭게 리렌더링 하면서 성능을 아끼고, 최적의 사용자 경험을 제공 할 수 있을까요? 이를 이해하려면 '초기렌더링'과 '리렌더링' 개념을 이해해야 합니다.

 

1) 초기 렌더링

컴포넌트의 render 함수를 통하여 렌더링이 이루어지게 됩니다. 렌더함수는 뷰가 어떻게 생겼고 어떻게 작동하는지에 대한 정보를 지닌 객체를 반환합니다. render 함수가 실행하면 렌더링이 이루어지고 HTML 마크업(markup) 을 만들고 이를 DOM 요소 안에 주입하는 작업이 이루어 지게 됩니다.

 

2) 리렌더링(조화 과정)

뷰를 업데이트 할적에 "업데이트 과정을 거친다" 라고 하기보다 "조화 과정을 거친다" 라고 하는 것이 더 정확한 표현입니다. 이유는 새로운 DOM 요소로 갈아 끼우기 때문입니다.

리액트는 데이터가 변경되면 render 함수가 반환하는 결과를 곧바로 DOM 에 반영하지 않고, 이전에 render 함수가 만들었던 컴포넌트 정보와 현재 render 함수가 만든 컴포넌트 정보를 비교한후 DOM 트리를 업데이트 하게 됩니다.

 

리액트의 특징

리액트의 주요 특징 중 하나는 Virtual DOM을 사용하는 것

 

우선 DOM 이 무엇인지 부터 알아보면.. DOM 이란 문서객체 모델(Document Object Model)의 약어입니다. 웹 브라우저는 DOM 을 활용하여 객체에 자바스크립트와 CSS 를 적용할 수 있습니다. DOM 은 트리형태로 구성되어 있는데 DOM 을 통해서 특정 노드를 찾거나 수정하거나 제거하거나 원하는 곳에 삽입할 수 있다.

 

이미지출처: 위키백과

DOM 은 과연 느릴까?

DOM 에는 치명적인 한 가지 문제점이 있습니다. 바로 동적 UI에 최적화되어 있지 않다는 것입니다.

"요즈음 자바스크립트 엔진은 매우 빠른 반면, DOM 은 느리다?" 라고 하는데 이는 정확한 말이 아닙니다.

DOM 자체는 빠릅니다. 단 웹 브라우저 단에서 DOM 에 변화가 일어나면 웹브라우저가 CSS를 다시 연산하고, 레이아웃을 구성하고, 페이지를 리페인트하게되는데 이 과정에서 시간이 허비되는 것입니다. 이에 대한 해결법으로 DOM을 최소한으로 조작하여 작업을 처리하는 방식으로 개선할 수 있습니다.

 

Virtual DOM

이미지출처:&nbsp;speakerdeck

리액트는 Virtual DOM 방식을 사용하여 DOM 업데이트를 추상화함으로써 DOM 처리 횟수를 최소화하고 효율적으로 업데이트를 진행할 수 있습니다. Virtual DOM 을 사용하면 실제 DOM에 접근하여 조작하는 대신, 이를 추상화한 자바스크립트 객체를 구성하여 사용하게 됩니다. 이는 마치 실제 DOM의 가벼운 사본과 비슷합니다..

 

리액트에서 실제 DOM을 업데이트 할 때 세가지 절차를 밟게 되는데 살펴보면..

1. 데이터를 업데이트하면 전체 UI를 Virtual DOM에 리렌더링 한다.

2. 이전 Virtual DOM에 있던 내용과 현재 내용을 비교한다.

3. 바뀐 부분만 실제 DOM에 적용한다.

 

이처럼  리액트는 Virtual DOM 을 통해서 동적 UI 최적화 문제를가지고 있는 DOM 업데이트 방식을 보안하기 때문에 효율적으로 작업을 진행 할 수 있습니다.

 

또한 일부 웹 프레임워크가 MVC, MVW 등의 구조를 지향하는 것과 달리 리애트는 오직 뷰만 담당하는 라이브러리 로서 취향대로 스택을 설정할 수 있다는 장점을 가지고 있지만 여러 라이브러리를 접해야 한다는 단점을 가지고 있기도 합니다.

+ Recent posts