스프링 웹 개발은 다음과 같은 3가지 방법이 있다.

 

  1. 정적 컨텐츠 : 서버에서 특별히 뭔가 하는 거 없이 단순히 html파일을 클라이언트 쪽으로 넘기는 방식이다. 
  2. MVC & 템플릿 엔진 : 서버에서 특정한 작업을 통해 html파일을 동적으로 만들어 넘기는 방식이다. JSP, PHP 등의 템플릿 엔진이 사용되며 이걸 하기 위해 MVC패턴이 도입된다.
  3. API : JSON 포맷 등으로 '데이터'를 클라이언트에 전달하는 방식이다. 서버 간의 통신에도 활용되며, 요즘은 React같은 애들한테 API로 데이터를 넘기고 그걸 통해 화면을 클라이언트가 그리는 식으로도 활용된다.

 

정적 컨텐츠

resources폴더의 static폴더에 있는 파일을 찾아 넘긴다. 다음 순서로 동작한다고 이해할 수 있다.

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 없으면 내부의 resources/static에서 찾아서 넘김

 

MVC & 템플릿 엔진

MVC는 Model, View, Controller로 각자의 역할을 구분해 개발하는 방식을 일컫는다. Model과 Controller는 비즈니스 로직과 내부 로직을 처리하는 역할을 맡고, View는 화면을 그리는 역할을 맡는다. 옛날엔 JSP를 통해서 View에서 Controller의 역할도 수행했는데 이를 model 1방식이라 부르기도 함.

 

@Controller
public class HelloController {
    @GetMapping("hello")
    public String hello(Model model) {
        model.addAttribute("data", "hello!!");
        return "hello";
    }
}

 

  • GetMapping : GET POST 할 때의 get이며, /hello로 get요청이 들어왔을 때 hello라는 메서드를 실행한다는 거다. 
  • Model : View를 그리기 위해 사용되는 데이터들의 박스(?)로, key-value형태로 데이터들을 저장한다. 내부적으론 Map이 사용된다고 함.
  • return "hello" : 데이터들을 보낼 View이름을 지정하는 거라고 보면 된다.

 

 

이 방식의 동작순서는 다음과 같다.

 

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 있으면 해당 메서드를 호출
  5. viewResolver가 지정된 이름의 View를 찾아주고 템플릿 엔진과 연결시켜줌
  6. 템플릿엔진이 렌더링한 html파일을 반환

 

API

이 방식은 html파일이 아니라 데이터를 넘기는 방식이라고 볼 수 있다. 결국 정적 컨텐츠를 넘기는게 아니라면

 

  • html로 내려주든가
  • api로 (즉 데이터로) 주든가

 

둘 중에 하나인 거다.

 

@Controller
public class HelloController {
    @GetMapping("hello-string")
    @ResponseBody
    public String helloString(@RequestParam("name") String name) {
        return "hello" + name;
    }
}
  • GetMapping : GET POST 할 때의 get이며, /hello-string로 get요청이 들어왔을 때 helloString라는 메서드를 실행한다는 거다. 
  • @ResponseBody : 이 어노테이션을 쓰면 viewResolver를 쓰지 않고, 대신에 Http Response의 BODY에 데이터를 담게 된다.
  • @RequestParam("name") String name : 쿼리에서 "name"에 해당하는 값을 name이란 파라미터로 받아온다는 거다.
  • return "hello"  + name : 이 문자열 자체를 데이터로 준다는거다.

 

참고로 객체를 반환하면 객체가 JSON으로 변환되서 보내진다!

@Controller
public class HelloController {
    @GetMapping("hello-api")
    @ResponseBody
    public Hello helloApi(@RequestParam("name") String name) {
        Hello hello = new Hello();
        hello.setName(name);
        return hello;
    }

    static class Hello {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

이 방식의 동작순서는 다음과 같다.

 

  1. url를 입력
  2. 내장된 톰캣이 요청을 받고 스프링 컨테이너에게 요청을 넘김
  3. 컨트롤러가 url에 매핑되는 메서드가 있는지 먼저 찾음
  4. 있으면 해당 메서드를 호출
  5. 근데 @ResponseBody가 있으면 viewResolver에게 전달하지 않고 httpMessageConverter에게 전달함
  6. 문자는 그대로 보내고, 객체는 JSON으로 바꿔서 전달한다.

 

 

용어 정리

Gradle : 라이브러리들을 버전 설정하고 가져와주는 빌드 도구. 은 의존관계가 있는 라이브러리들도 함께 다운해준다(A라이브러리가 B에 의존하면 자동으로 B도 땡겨줌).

 

resources폴더 : 자바 파일(*.java)를 제외한 나머지 파일들을 위한 폴더

 

템플릿 엔진 : 지정된 템플릿 양식과 데이터를 합쳐 HTML 문서를 출력하는 SW. 예를 들어  그냥 a.html을 작성하기만 하면 서버에선 그냥 걔를 클라이언트쪽으로 넘기기만 하는데, 템플릿 엔진을 쓰면 html에서 루프를 쓴다던가 어떤 데이터값을 넣는다든가 할 수 있는 것이다. 

 

<html xmlns:th="http://wwww.thymeleaf.org">

 

html태그에 이렇게 작성해두면 해당 html파일에서 타임리프(템플릿 엔진) 문법을 사용 가능하다. 이를 활용해 서버에서 받아온 특정 데이터 값을 넣고 싶으면

 

<p th:text="'안녕하세요' + ${data}">안녕하세요.손님</p>

 

이런 식으로 하면 된다. 참고로 태그 사이엔 딱히 뭔가를 쓸 필욘 없음. 근데 템플릿 엔진을 거치지 않고 html파일을 열거나 할 때 태그 사이의 값이 나온다. 이미지 태그의 alt값과 비슷한 역할이라고 생각할 수 있겠다.

 

 

스프링 강의를 수강하기 시작하며 Optional이란 걸 마주쳤다! null값으로 올 수 있는 애들을 위해서 실무에서 많이 쓴다던데?

이제 이런 것들 안 지나치고 내 것으로 만들어야겠다는 생각이 든다. 바로 알아봤음.


일단 이 녀석은 Integer클래스처럼 일종의 Wrapper클래스다. 포장해주는 느낌. 그리고 그 알멩이로는 모든 타입의 변수가 저장될 수 있다.

 

내부 알멩이로 null 역시 저장될 수 있기 때문에, null값과 관련된 예상치 못한 NPE(NullPointerException)들을 피할 수 있다는게 가장 큰 장점이다. 예를 들면 어떤 저장소에서 find를 했는데 내가 찾는 게 없어서 null값이 반환된 경우 등..이런 상황을 방지하려면 find한 결과가 null인지 아닌지를 if문으로 파악해야 했지만 Optional을 사용하면 그런 걸 간단히 처리할 수 있다는 게 장점쓰. 애당초 null일 수도 있는 값을 다루기 위한 클래스이기 때문에 그와 관련된 다양한 메서드들을 제공한다.

 

그러면 일단, 이 Optional 객체를 어떻게 만드는가? Optional클래스를 뜯어보면 다음과 같이 of와 ofNullable이라는 스태틱 메서드가 정의돼있는 것을 볼 수 있다.

 

 

두 메서드 모두 Optional을 리턴하는 걸 볼 수 있다. 즉 이 두 메서드를 통해서 Optional객체를 생성할 수 있다! of메서드는 인자로 'null이 아닌 값'을 받고, ofNullable메서드는 인자로 'null값일 수도 있는 값'을 받는다.

 

of메서드의 경우 인자로 준 값이 null이면 NPE예외가 발생한다. 또한, ofNullable메서드의 경우 인자로 준 값이 null이면 EMPTY, 즉 빈 Optional객체가 리턴된다. 아니 그냥 내부에 null값 넣어서 리턴해주지 왜 약속된 빈 Optional객체(= EMPTY)를 리턴하도록 했을까? 답은 메모리 절약을 위해서. 어차피 알멩이가 null인 Optional 객체는 솔직히 의미없는 객체인데, 여러 곳에서 이 깡깡이 Optional객체를 만들었다면 메모리 낭비가 있을 것이다. 이를 위해 Optional클래스가 싱글턴 인스턴스로 EMPTY 객체를 가지는거고, 얘 하나만을 빈 객체로 리턴해주는 것이다. 개발자 분들이 메모리 절약을 위한 센스있는 선택을 하신 것 같다. 캬..

 

 

그럼 이 Optional객체가 감싸고 있는 알멩이는 어떻게 꺼낼 수 있을까? 간단하다. get메서드로 꺼낼 수 있다. 근데 음..이렇게 꺼내쓰는 건 권장되지 않는다고 한다. 만약 Optional객체가 감싸고 있는 값이 null이면 NoSuchElementException예외가 발생하기 때문. 즉 다짜고짜 꺼내는 게 아니라 안에 있는 알멩이가 null인지를 확인하고 꺼내는 게 좋다. isPresent메서드가 이 기능을 해준다.

 

참고로 isEmpty메서드는 는 반대로 알멩이가 null이면 true를 리턴한다

 

그럼 만약 알멩이가 null인지를 확인하고, null이 아니면 바로 뭔가 해주고 싶으면? ifPresent메서드를 사용할 수도 있다. 이 메서드는 알멩이가 null이 아니면 파라미터로 받은 action을 실행해준다.

 

뭐 람다식같은 거 인자로 주면 되겠죵?

 

만약, 알멩이를 꺼내고 싶은데 알멩이가 null이면 내가 지정한 값을 쓰고 싶다면? orElse메서드를 쓰면 된다.

 

 

 

이외에도 여러 메서드들이 있다. 실무에서는 null값이 될 수도 있는 애들을 Optional로 래핑해서 처리하는 경우가 많다고 한다. 나도 이 방법을 숙지하도록 하자. 아자아자..!

 

※ 참고 : orElse vs orElseGet에 관련된 좋은 글

https://ysjune.github.io/posts/java/orelsenorelseget/

 

orElse 와 orElseGet 무슨 차이가 있을까?

The Lazy Snack's Cargo

ysjune.github.io

 

firebase와 연계해서 프로젝트를 진행하던 중, 로그인이후 Main page로 이동시킬 때 로그인된 user의 uid를 같이 보내야했다. 이를 어떻게 할 수 있을까?


UseNavigate

react router에서 지원하는 Hook. 지정한 경로로 페이지를 이동시킬 수 있다. 근데 두 번째 인자로 이동시킬 페이지에 함께 보낼 데이터를 지정할 수 있다! 예시는 다음과 같다.

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

// ...생략

const navigate = useNavigate();

// ...생략

const gotoMain = () => {
  navigate("/main", {
    state: {
      userId: user.uid
    }
  });
};

참고로 이렇게 데이터를 보낼 때는 두 번째 인자의 state라는 속성으로 보내야 한다고 함. 

 

UseLocation

react router에서 지원하는 Hook. useNavigate를 이용해 전송된 데이터를 받을 수 있다. 예시는 다음과 같다.

import { useLocation } from 'react-router-dom'; 

// ...생략

const location = useLocation();

// ...생략

const [userId, setUserId] = useState(
  location.state?.userId
);

location의 state프로퍼티에 내가 보낸 데이터들이 담겨있다고 보면 된다. 

 

 

 

 

참고로, Link태그를 통해서도 데이터를 보낼 수 있다고 한다. 예시는 다음과 같다.

<Link to={`/main`} state={{ test: "hello world" }} >
   test
</Link>

 

또한 참고로, useNavigate의 두 번째 인자로는 state가 아니라 replace라는 것도 보낼 수 있다.

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

// ...생략

const navigate = useNavigate();

// ...생략

const gotoMain = () => {
  navigate("/main", {replace: true});
};

기본값은 false이고, true로 돼있으면 페이지가 이동된 뒤 뒤로가기를 하더라도 방금 페이지로 돌아올 수 없고, 루트로 돌아오게 된다고 한다. false는 뒤로가기가 가능.

 

강의를 들으면서 Ref를 통해 Input태그의 값을 받아오는 걸 보곤 했지만, 무슨 이유로 Ref를 쓰는지는 정확히 몰랐었다.

이번 기회에 정리해보자.


Ref

리액트에서 원하는 시점에 실제 DOM노드에 접근하고 싶을 때 사용하는 놈이다. 다음과 같이 Ref객체를 생성할 수 있다.

import { useRef } from 'react';

// ...생략

const inputRef = useRef();

그리고, 만들어진 Ref객체는 다음과 같이 원하는 DOM요소에 ref속성을 통해 연결시킬 수 있다.

<input type="text" name="title" ref=[inputRef} />

이 Ref라는 객체는 console.log로 찍어보면 알 수 있지만 current라는 프로퍼티를 가지며, current프로퍼티는 ref속성으로 연결된 DOM노드를 가리킨다. 즉 Ref객체의 current속성을 이용해 연결된 DOM노드에 접근하여 value등을 읽어올 수 있는 것!

 

다만 주의할 점은, DOM노드는 렌더링이 끝나야만 생기니까 Ref객체의 current값 역시 컴포넌트가 화면에 렌더링됐을 때만 존재한다는 것. 따라서 Ref를 통해 DOM노드에 접근할 때는 Ref.current를 확인한 다음 사용하도록 하자.

 

그래..이래서 Ref의 current로 인풋 태그의 값을 읽어오고 뭐 그랬던 거구나~으흠~

한마디로 Ref를 사용하면 DOM노드를 직접적으로 건드릴 수 있구나~(포커스를 준다던가..)

 

..? 근데 Ref 왜 쓰는 거지?

자바스크립트에선 querySelector로 그냥 잡아올 수 있는데 말이다.


Ref를 쓰는 이유

id, class를 통해서 바닐라JS로 DOM조작이 가능한 건 맞지만, 컴포넌트가 여러 개 생긴 경우 id나 class로 원하는 DOM요소를 가져오는 건 매우 힘들다. 이 때 Ref를 통해 원하는 DOM요소를 특정하도록 할 수 있다.

 

기본 : input태그의 value프로퍼티로 input태그의 값을 가져올 수 있다. 


다음과 같이 input태그들의 값을 담을 state를 만든다.

const [values, setValues] = useState({
  title: '',
  writer: '',
  content: '',
});

tite, writer, content는 각 input태그들이 갇는 name프로퍼티다. 편의를 위해 통일시킨 것임.

그리고 다음과 같이 handleChange함수를 만든다.

const handleChange = (e) => {
  setValues(prevValues => {
    const { name, value } = e.target;
    return {
      ...prevValues,
      [name]: value
    };
  };
};

각 input 태그들에 다음과 같이 onChange프로퍼티로 멕여준다.

<form>
  <input type="text" name="title" value={values.title} onChange={handleChange} />
  <input type="text" name="writer" value={values.writer} onChange={handleChange} />
  <input type="text" name="content" value={values.content} onChange={handleChange} />
  <button type="submit">제출</button>
</form>

리액트에서의 onChange에서는 순수JS에서의 oninput이벤트와 같다. 즉 뭔가 입력할 때마다 발생! 이로 인해 input태그의 값들과 values state는 항상 같은 값을 지니게 된다.

 

그리고 제출 버튼을 누르면 동작할 form태그의 submit이벤트에 대해선 고냥 values state의 값들 이용하면 된다. 이 방법을 배우기 전까지는 제출 버튼이 눌리면 그 때 input태그의 값들을 읽어오는 방식을 써야 된다고 생각했는데, 버튼이 눌리기 전에 위에서 한 것처럼 state에 input태그 값들을 onChange를 통해서 일치시키고 제출이 눌리면 바로 state를 활용하는 방법을 쓸 수도 있다는 걸 배웠다.

 

참고로 form태그는 submit이벤트에 대해 기본적으로 GET메소드로 input태그들의 값을 보내니까 이를 방지할 것. 방지하는 방법은 e.preventDefault()를 쓰면 됨.

 

또한 위 코드에서 input태그들에 value 프로퍼티를 딱히 줄 필욘 없음. 저게 없어도 input태그 값 가져오는 건 가능하니까. 다만 저렇게 하면 항상 state값 == input태그값이 됨 즉 input태그의 값이 제어되는 효과를 준다. 

예를 들어, input태그에서 소문자로 입력을 해도 setState에서는 state에 upper를 해서 저장해둔다면 input태그의 값들도 대문자로 알아서 바뀌는 효과를 줄 수 있음. 즉 input태그의 값이 제어되는 것! 이런 컴포넌트를 controlled component라 하고, 쓰나 안 쓰나라면 controlled component로 하는 게 좋다고 한다.

 

 

※ 참조

https://ko.reactjs.org/docs/forms.html

 

폼 – React

A JavaScript library for building user interfaces

ko.reactjs.org

 

+ Recent posts