레이어드 아키텍처(Layered Architecture) 실습(2)

최준영·2021년 10월 1일
0

1. maven project 생성

  • maven-archetype-webapp으로 생성
  • group id, artifact id는 패키지가 될 것이기 때문에 소문자로 지정
  • artifact id는 실제 프로젝트 이름을 기입

2. pom.xml에 해당 코드 추가

  <properties>
    <spring.version>5.2.5.RELEASE</spring.version>
    <!-- jackson -->
    <jackson2.version>2.9.4</jackson2.version>
  </properties>

  <dependencies>
    <!-- Spring -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- Servlet JSP JSTL -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>javax.servlet.jsp-api</artifactId>
      <version>2.3.1</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>javax.servlet-api</artifactId>
      <version>3.1.0</version>
      <scope>provided</scope>
    </dependency>

    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>jstl</artifactId>
      <version>1.2</version>
    </dependency>

    <!-- spring jdbc & jdbc driver & connection pool -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.26</version>
    </dependency>

    <!-- basic data source -->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-dbcp2</artifactId>
      <version>2.1.1</version>
    </dependency>

    <!-- Jackson Module -->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>${jackson2.version}</version>
    </dependency>

    <dependency>
      <groupId>com.fasterxml.jackson.datatype</groupId>
      <artifactId>jackson-datatype-jdk8</artifactId>
      <version>${jackson2.version}</version>
    </dependency>
  </dependencies>

      <plugins>
        <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.6.1</version>
          <configuration>
            <source>1.8</source>
            <target>1.8</target>
          </configuration>
        </plugin>
      </plugins>
  • spring 버전은 5.1 이상으로 해야 오류가 발생하지 않는다.
  • JDBC는 컴퓨터에 깔려있는 mysql 버전과 동일해야한다.

pom.xml 저장 후 maven update

3. org.eclipse.wst.common.project.facet.core.xml 수정

  • Navigator -> .settings에 해당 파일 존재
<?xml version="1.0" encoding="UTF-8"?>
<faceted-project>
  <fixed facet="wst.jsdt.web"/>
  <installed facet="jst.web" version="3.1"/> <!--  이부분 수정 -->
  <installed facet="wst.jsdt.web" version="1.0"/>
  <installed facet="java" version="1.8"/>
</faceted-project>
  • 수정 후 이클립스 재시작
  • properties -> project Facets에서 dynamic web module이 3.1인지 확인한다.
  • dynamic project로 생성 후 maven project로 컨버트하는 쉬운 방법이 있긴하다.

4. main폴더 안에 java, resources 폴더 생성

5. config 파일 생성

  • java 폴더 내에 config 패키지에 추가

WebMvcContextConfiguration 클래스 생성

  • 클래스 생성할 때 WebMvcConfigurerAdapter를 상속받게 한다.
  • WebMvcContextConfiguration.java
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {"kr.or.connect.guestbook.controller"})
public class WebMvcContextConfiguration extends WebMvcConfigurerAdapter {
  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/css/**").addResourceLocations("/css/").setCachePeriod(31556926);
    registry.addResourceHandler("/img/**").addResourceLocations("/img/").setCachePeriod(31556926);
    registry.addResourceHandler("/js/**").addResourceLocations("/js/").setCachePeriod(31556926);
  }

  // default servlet handler를 사용하게 합니다.
  @Override
  public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
    configurer.enable();
  }

  @Override
  public void addViewControllers(final ViewControllerRegistry registry) {
    System.out.println("addViewControllers가 호출됩니다. ");
    registry.addViewController("/").setViewName("index");
  }

  @Bean
  public InternalResourceViewResolver getInternalResourceViewResolver() {
    InternalResourceViewResolver resolver = new InternalResourceViewResolver();
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".jsp");
    return resolver;
  }
}
  • 해당 설정은 dispatcher Servlet이 읽어들인다.

@Configuration

  • 해당 클래스가 설정이라는 것을 알려준다.

@EnableWebMvc

  • 기본적인 설정들을 자동으로 해준다.

@ComponentScan

  • Controller 같은 것들을 알아서 읽어오게 한다.

addResourceHandlers

  • URL요청이 /css, /img, /js로 들어오는 것들을 어떤 폴더에서 읽어오도록 설정한다.

configureDefaultServletHandling

  • default servlet handler를 사용할 수 있도록 하는 설정이다.
  • 매핑 정보가 없는 URL 요청이 들어왔을 때, 해당 요청을 spring의 DefaultServletHttpRequestHandler가 처리하도록 한다.
  • 이렇게 설정함으로써 매핑이 없는 URL이 넘어왔을 때 WAS의 default servlet이 static한 자원을 읽어서 보여줄수 있게끔 한다.

addViewController

  • 특정 URL에 대한 처리를 컨트롤러 클래스를 작성하지 않고 매핑할 수 있도록 한다.
  • 여기서는 /라고 요청이 들어오면 index라는 이름의 뷰로 보여달라는 의미이다.

InternalResourceViewResolver

  • 적절한 view resolver가 실제로 뷰의 이름을 가지고 어떤 뷰인지에 대한 정보를 찾아낼 수 있게 한다.
  • 여기에서는 resolver에 prefix와 suffix를 지정함으로써, /WEB-INF/views/뷰이름.jsp로 변환한다.

DBConfig 클래스 생성

  • 생성할 때 TransactionManagementConfigurer을 구현하도록 한다.
  • Spring JDBC가 읽어낼 수 있도록, 데이베이스에 관련된 설정이다.
  • DBConfig.java
@Configuration
@EnableTransactionManagement
public class DBConfig implements TransactionManagementConfigurer {
  private String driverClassName = "com.mysql.cj.jdbc.Driver";

  private String url = "jdbc:mysql://localhost:3306/connectdb?useUnicode=true&characterEncoding=utf8";

  private String username = "connectuser";

  private String password = "connect123!@#";

  @Bean
  public DataSource dataSource() {
    BasicDataSource dataSource = new BasicDataSource();
    dataSource.setDriverClassName(driverClassName);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
  }

  @Override
  public PlatformTransactionManager annotationDrivenTransactionManager() {
    return transactionManger();
  }

  @Bean
  public PlatformTransactionManager transactionManger() {
    return new DataSourceTransactionManager(dataSource());
  }
}

@EnableTransactionManagement

  • 트랜잭션과 관련된 설정을 자동으로 해준다.
  • 사용자 간의 트랜잭션 처리를 위한 PlatformTransactionManager를 설정하기 위해 다음을 해야한다.
    • TransactionManagement Configurer를 구현
    • annotationDrivenTransactionManager 메서드를 오버라이딩
      • 해당 메서드에서 트랜잭션을 처리할 PlatformTransactionManager 객체를 반환하게 한다.

필드와 dataSource()

  • 데이터베이스 연결을 위해 DataSource Bean을 등록한다.

ApplicationConfig 클래스 생성

  • ApplicationConfig.java
@Configuration
@ComponentScan(basePackages = { "kr.or.connect.guestbook.dao",  "kr.or.connect.guestbook.service"})
@Import({ DBConfig.class })
public class ApplicationConfig {

}

@ComponentScan

  • dao나 service에 구현되어있는 컴포턴트들을 읽어오기 위해 basePackages에 각각의 패키지를 지정한다.

@Import

  • 수행될 때 내부적으로 DBConfig에 사용되고 있는 것들을 쓰기 위해 해당 부분을 넣어준다.

Web.xml 수정

<?xml version="1.0" encoding="UTF-8"?>

<web-app>
  <display-name>Spring JavaConfig Sample</display-name>
  <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext
    </param-value>
  </context-param>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>kr.or.connect.guestbook.config.ApplicationConfig
    </param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener
    </listener-class>
  </listener>

  <servlet>
    <servlet-name>mvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext
      </param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>kr.or.connect.guestbook.config.WebMvcContextConfiguration
      </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>mvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>encodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter
    </filter-class>
    <init-param>
      <param-name>encoding</param-name>
      <param-value>UTF-8</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>encodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

servlet 태그

  • servlet-class에서 DispatcherServlet을 프론트 서블릿으로 등록하고있다.
  • 첫번째 init-param은 해당 ApplicationContext를 이용한다는 의미이다.
  • 두번재 init-param은 DispatcherServlet이 실행될 때 WebMvcContextConfiguration에 들어있는 설정을 참고하라는 의미이다.

listener 태그

  • 프레젠테이션 부분과 나머지 부분들을 분리시키기 위해 비즈니스 로직 쪽에서 사용되는 것은 DBConfig와 ApplicationConfig에 나누어 두었다. 이 부분을 읽기위해 사용한다.
  • listener는 어떤 특정한 이벤트가 일어났을 때 동작한다.
  • ContextLoaderListener로 되어있는데, run on server 이후 Context가 로딩될 때 이것을 읽어서 수행하라는 의미이다.
    • 즉, Context가 로딩되는 이벤트가 일어났을 때, 이 클래스를 실행해달라는 의미이다.

context-param 태그

  • DispatcherServlet이 실행될 때 init-param에 등록된 파일을 가져다 사용하는 것처럼, 리스너가 실행될 때 context-param에 등록된 부분들을 참고한다.
  • 첫번째 param은 ApplicationContext를 AnnotationConfigWebApplicationContext로 이용한다는 설정이다.
  • 두번째 param은 ApplicationConfig을 읽어들인다는 설정이다.

filter 태그

  • filter는 요청이 수행되기 전, 응답이 나가기 전 각각 한 번씩 수행한다.
  • encodingFilter을 UTF-8로 지정하여 인코딩시 한글 깨짐을 막아준다.
  • 모든 요청에 적용하기 위해 url-pattern을 /*로 지정한다.
    • 특정 URL에만 지정하게 할 수 있다.

6. Dto 생성

  • dto 패키지에 추가

Guestbook.java

package kr.or.connect.guestbook.dto;

import java.util.Date;

public class Guestbook {
  private Long id;
  private String name;
  private String content;
  private Date regdate;
  
  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getContent() {
    return content;
  }
  public void setContent(String content) {
    this.content = content;
  }
  public Date getRegdate() {
    return regdate;
  }
  public void setRegdate(Date regdate) {
    this.regdate = regdate;
  }
  
  @Override
  public String toString() {
    return "Guestbook [id=" + id + ", name=" + name + ", content=" + content + ", regdate="
        + regdate + "]";
  }
}

Log.java

package kr.or.connect.guestbook.dto;

import java.util.Date;

public class Log {
  private Long id;
  private String ip;
  private String method;
  private Date regdate;
  
  public Long getId() {
    return id;
  }
  public void setId(Long id) {
    this.id = id;
  }
  public String getIp() {
    return ip;
  }
  public void setIp(String ip) {
    this.ip = ip;
  }
  public String getMethod() {
    return method;
  }
  public void setMethod(String method) {
    this.method = method;
  }
  public Date getRegdate() {
    return regdate;
  }
  public void setRegdate(Date regdate) {
    this.regdate = regdate;
  }
  
  @Override
  public String toString() {
    return "Log [id=" + id + ", ip=" + ip + ", method=" + method + ", regdate=" + regdate + "]";
  }
}

7. Dao 생성

  • dao 패키지에 추가

LogDao.java

package kr.or.connect.guestbook.dao;

import javax.sql.DataSource;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import kr.or.connect.guestbook.dto.Log;

@Repository
public class LogDao {
  private NamedParameterJdbcTemplate jdbc;
  private SimpleJdbcInsert insertAction;
  
  public LogDao(DataSource dataSource) {
    this.jdbc = new NamedParameterJdbcTemplate(dataSource);
    this.insertAction = new SimpleJdbcInsert(dataSource)
        .withTableName("log").usingGeneratedKeyColumns("id");
  }
  
  public Long insert(Log log) {
    SqlParameterSource params = new BeanPropertySqlParameterSource(log);
    return insertAction.executeAndReturnKey(params).longValue();
  }
}
  • .usingGeneratedKeyColumns("id");은 id column의 값을 자동으로 입력하도록 설정한다.
  • 실제 사용할 때 insertAction.executeAndReturnKey 메서드는 insert 문은 내부적으로 생성해서 실행을 하고 자동으로 생성된 id 값을 리턴하게 된다.

GuestbookDaoSqls.java

package kr.or.connect.guestbook.dao;

public class GuestbookDaoSqls {
  public static final String SELECT_PAGING = "SELECT id, name, content, regdate FROM guestbook ORDER BY id DESC limit :start, :limit";
  public static final String DELETE_BY_ID = "DELETE FROM guestbook WHERE id = :id";
  public static final String SELECT_COUNT = "SELECT count(*) FROM guestbook";
}
  • sql만 따로 분류
  • limit 시작 인덱스, 반환 개수를 설정해서 특정한 부분만 select 해올 수 있다.

GuestbookDao.java

package kr.or.connect.guestbook.dao;

import static kr.or.connect.guestbook.dao.GuestbookDaoSqls.DELETE_BY_ID;
import static kr.or.connect.guestbook.dao.GuestbookDaoSqls.SELECT_COUNT;
import static kr.or.connect.guestbook.dao.GuestbookDaoSqls.SELECT_PAGING;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Repository;
import kr.or.connect.guestbook.dto.Guestbook;

@Repository
public class GuestbookDao {
  private NamedParameterJdbcTemplate jdbc;
  private SimpleJdbcInsert insertAction;
  private RowMapper<Guestbook> rowMapper = BeanPropertyRowMapper.newInstance(Guestbook.class);

  public GuestbookDao(DataSource dataSource) {
    this.jdbc = new NamedParameterJdbcTemplate(dataSource);
    this.insertAction =
        new SimpleJdbcInsert(dataSource).withTableName("guestbook").usingGeneratedKeyColumns("id");
  }

  public List<Guestbook> selectAll(Integer start, Integer limit) {
    Map<String, Integer> params = new HashMap<>();
    params.put("start", start);
    params.put("limit", limit);
    return jdbc.query(SELECT_PAGING, params, rowMapper);
  }


  public Long insert(Guestbook guestbook) {
    SqlParameterSource params = new BeanPropertySqlParameterSource(guestbook);
    return insertAction.executeAndReturnKey(params).longValue();
  }

  public int deleteById(Long id) {
    Map<String, ?> params = Collections.singletonMap("id", id);
    return jdbc.update(DELETE_BY_ID, params);
  }

  public int selectCount() {
    return jdbc.queryForObject(SELECT_COUNT, Collections.emptyMap(), Integer.class);
  }
}
  • 여기서 sql문 변수를 사용할 때, import static으로 불러와서 클래스명 없이 바로 변수명으로 사용함

8. Service 생성

GuestbookService.java - 인터페이스

package kr.or.connect.guestbook.service;

import java.util.List;
import kr.or.connect.guestbook.dto.Guestbook;

public interface GuestbookService {
  public static final Integer LIMIT = 5;
  public List<Guestbook> getGuestbooks(Integer start);
  public int deleteGuestbook(Long id, String tp);
  public Guestbook addGuestbook(Guestbook guestbook, String ip);
  public int getCount();
}
  • Service 패키지에 추가

GuestbookServiceImpl.java - 구현체

  • Service.impl에 추가
package kr.or.connect.guestbook.service.impl;

import java.util.Date;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import kr.or.connect.guestbook.dao.GuestbookDao;
import kr.or.connect.guestbook.dao.LogDao;
import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.dto.Log;
import kr.or.connect.guestbook.service.GuestbookServie;

@Service
public class GuestbookServiceImple implements GuestbookService{
  @Autowired
  GuestbookDao guestbookDao;
  
  @Autowired
  LogDao logDao;

  @Override
  @Transactional
  public List<Guestbook> getGuestbooks(Integer start) {
    List<Guestbook> list = guestbookDao.selectAll(start, GuestbookServie.LIMIT);
    return list;
  }

  @Override
  @Transactional(readOnly = false)
  public int deleteGuestbook(Long id, String ip) {
    int deleteCount = guestbookDao.deleteById(id);
    Log log = new Log();
    log.setIp(ip);
    log.setMethod("delete");
    log.setRegdate(new Date());
    logDao.insert(log);
    return deleteCount;
  }

  @Override
  @Transactional(readOnly = false)
  public Guestbook addGuestbook(Guestbook guestbook, String ip) {
    guestbook.setRegdate(new Date());
    Long id = guestbookDao.insert(guestbook);
    guestbook.setId(id);
//  if(1 == 1)
//  throw new RuntimeException("test exception");
//  
    Log log = new Log();
    log.setIp(ip);
    log.setMethod("insert");
    log.setRegdate(new Date());
    logDao.insert(log);
    return guestbook;
  }

  @Override
  public int getCount() {
    return guestbookDao.selectCount();
  }
}
  • 클래스 위에 @Service를 붙인다.

@Transactional

  • 내부적으로 readOnly라는 형태로 connection을 사용하게 된다.
    • 해당 메서드가 readOnly가 아니라면 트랜잭션이 적용이 되지 않기 때문에 readOnly를 false로 변경한다.

9. Controller

  • GuestbookController.java
package kr.or.connect.guestbook.controller;

import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import kr.or.connect.guestbook.dto.Guestbook;
import kr.or.connect.guestbook.service.GuestbookService;

@Controller
public class GuestBookController {
  @Autowired
  GuestbookService guestbookService;

  @GetMapping(path = "/list")
  public String list(@RequestParam(name = "start", required = false, defaultValue = "0") int start,
      ModelMap model) {
    // start로 시작하는 방명록 목록 구하기
    List<Guestbook> list = guestbookService.getGuestbooks(start);
    
    // 전체 페이지수 구하기
    int count = guestbookService.getCount();
    int pageCount = count / GuestbookService.LIMIT;
    if (count % GuestbookService.LIMIT >0)
      pageCount++;
    
    // 페이지 수만큼 start의 값을 리스트로 저장
    // 예를 들면 페이지 수가 3이면
    // 0, 5, 10 이렇게 저장된다.
    // list?start=0, list?start=5, list?start=10으로 링크가 걸린다.
    List<Integer> pageStartList = new ArrayList<>();
    for (int i = 0; i < pageCount; i++) {
      pageStartList.add(i * GuestbookService.LIMIT);
    }
    
    model.addAttribute("list", list);
    model.addAttribute("count", count);
    model.addAttribute("pageStartList", pageStartList);
    
    return "list";
  }
  
   @PostMapping(path="/write")
  public String write(@ModelAttribute Guestbook guestbook, HttpServletRequest request) {
     String clientIp = request.getRemoteAddr();
     System.out.println("clientIp : " + clientIp);
     guestbookService.addGuestbook(guestbook, clientIp);
     return "redirect:list";
   }
}
  • GuestbookService guestbookService;를 선언하고 @Autowired를 붙여주면 서비스 객체를 사용할 수 있다.
  • @RequestParam에서 defaultValue는 만약 값이 없다면 해당 값으로 준다는 설정이다.

10. views

  • list.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>방명록 목록</title>
</head>
<body>

	<h1>방명록</h1>
	<br> 방명록 전체 수 : ${count }
	<br>
	<br>

	<c:forEach items="${list}" var="guestbook">

${guestbook.id }<br>
${guestbook.name }<br>
${guestbook.content }<br>
${guestbook.regdate }<br>

	</c:forEach>
	<br>

	<c:forEach items="${pageStartList}" var="pageIndex" varStatus="status">
		<a href="list?start=${pageIndex}">${status.index +1 }</a>&nbsp; &nbsp;
</c:forEach>

	<br>
	<br>
	<form method="post" action="write">
		name : <input type="text" name="name"><br>
		<textarea name="content" cols="60" rows="6"></textarea>
		<br> <input type="submit" value="등록">
	</form>
</body>
</html>
  • c:forEach의 varStatus는 인덱스로 보면 된다.
  • &nbsp;은 공백은 나타내는 특수문자이다. 일반적인 띄어쓰기는 웹에 출력이 되지 않는다.
  • textarea은 여러 줄의 긴 문장을 입력할 수 있는 양식이다.
profile
do for me

0개의 댓글