Core ํ๊ทธ, Formatting ์์, fn ๋ด์ฅ ํจ์ ๋ฐ EL(Expression Language) ๋ฌธ๋ฒ ํต ๋ ํผ๋ฐ์ค
## 1. Taglib ์ ์ธ & ํ๊ฒฝ ์ค์ (Directives)
```xml
<!-- 1. ํ์ค JSTL 1.2 ์ ์ธ (Java EE / javax ๊ธฐ๋ฐ) -->
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
<%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
<%@ taglib prefix="x" uri="http://java.sun.com/jsp/jstl/xml" %>
<!-- 2. Jakarta EE 9+ ์ ์ธ (Spring Boot 3.x / Tomcat 10+) -->
<%@ taglib prefix="c" uri="jakarta.tags.core" %>
<%@ taglib prefix="fmt" uri="jakarta.tags.fmt" %>
<%@ taglib prefix="fn" uri="jakarta.tags.functions" %>
<!-- Spring Boot 3 Gradle ์์กด์ฑ ์์ -->
<!-- implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api' -->
<!-- implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl' -->
```
## 2. ๋ณ์ ์ ์ธ & ์์ ์ถ๋ ฅ (c:set, c:out)
```xml
<!-- ๋ณ์ ์ ์ธ (๊ธฐ๋ณธ scope๋ page) -->
<c:set var="userName" value="${user.name}" />
<c:set var="userRole" value="ADMIN" scope="session" />
<!-- ๊ฐ์ฒด์ ํ๋กํผํฐ ์ง์ ์์ -->
<c:set target="${userDto}" property="status" value="ACTIVE" />
<!-- c:out: XSS ๊ณต๊ฒฉ ๋ฐฉ์ด ํ์ ์ถ๋ ฅ (escapeXml="true" ๊ธฐ๋ณธ ์ ์ฉ) -->
<c:out value="${userInput}" default="๋ฑ๋ก๋ ๋ด์ฉ์ด ์์ต๋๋ค." />
<!-- HTML ํ๊ทธ๋ฅผ ๊ทธ๋๋ก ๋ ๋๋งํด์ผ ํ ๋๋ง escapeXml="false" ์ฌ์ฉ -->
<c:out value="${board.htmlContent}" escapeXml="false" />
<!-- ๋ณ์ ์ ๊ฑฐ -->
<c:remove var="tempToken" scope="session" />
```
## 3. ์กฐ๊ฑด ๋ถ๊ธฐ ์ฒ๋ฆฌ (c:if, c:choose)
```xml
<!-- 1. ๋จ์ผ ์กฐ๊ฑด ๊ฒ์ฌ (c:if๋ else๊ฐ ์์์ ์ ์) -->
<c:if test="${not empty user}">
<p>ํ์ํฉ๋๋ค, <c:out value="${user.name}"/>๋!</p>
</c:if>
<!-- ๊ฒ์ฌ ๊ฒฐ๊ณผ๋ฅผ boolean ๋ณ์์ ์ ์ฅํ์ฌ ์ฌ์ฌ์ฉ -->
<c:if test="${user.age >= 19}" var="isAdult" scope="page" />
<!-- 2. ๋ค์ค ์กฐ๊ฑด ๋ถ๊ธฐ (if-else if-else / switch ๋ฌธ ๋์ฒด) -->
<c:choose>
<c:when test="${user.role eq 'ADMIN'}">
<span class="badge badge-red">์ต๊ณ ๊ด๋ฆฌ์</span>
</c:when>
<c:when test="${user.role eq 'MANAGER'}">
<span class="badge badge-blue">์ด์์</span>
</c:when>
<c:when test="${user.point >= 1000}">
<span class="badge badge-green">์ฐ์ ํ์</span>
</c:when>
<c:otherwise>
<span class="badge badge-gray">์ผ๋ฐ ํ์</span>
</c:otherwise>
</c:choose>
```
## 4. ์ปฌ๋ ์
& ์ซ์ ๋ฐ๋ณต ์ํ (c:forEach)
```xml
<!-- 1. ๋ฆฌ์คํธ/๋ฐฐ์ด ์ํ์ varStatus ์ํ๊ฐ ํ์ฉ -->
<c:forEach items="${userList}" var="item" varStatus="status">
<tr class="${status.first ? 'first-row' : ''} ${status.last ? 'last-row' : ''}">
<td>${status.count}</td> <!-- 1๋ถํฐ ์์ํ๋ ์๋ฒ (1, 2, 3...) -->
<td>${status.index}</td> <!-- 0๋ถํฐ ์์ํ๋ ์ธ๋ฑ์ค (0, 1, 2...) -->
<td><c:out value="${item.username}" /></td>
<td>${status.current.email}</td> <!-- ํ์ฌ ์ํ ๊ฐ์ฒด -->
</tr>
</c:forEach>
<!-- 2. ์ซ์ ๋ฒ์ ๋ฐ๋ณต (1๋ถํฐ 10๊น์ง 2์ฉ ์ฆ๊ฐ) -->
<c:forEach var="num" begin="1" end="10" step="2">
<span>${num}</span> <!-- 1, 3, 5, 7, 9 -->
</c:forEach>
<!-- 3. ๋ฌธ์์ด ๊ตฌ๋ถ์ ๋ถ๋ฆฌ ์ํ (c:forTokens) -->
<c:forTokens items="Java,Spring,JSP,Kotlin,SQL" delims="," var="tech">
<span class="tag">${tech}</span>
</c:forTokens>
```
## 5. URL ์์ฑ & ํ์ด์ง ์ด๋ (c:url, c:redirect)
```xml
<!-- c:url: Context Path ์๋ ๋ถ์ฐฉ ๋ฐ ํน์๋ฌธ์ ์์ ์ธ์ฝ๋ฉ -->
<!-- ๊ฒฐ๊ณผ ์์: /my-app/board/view?boardId=42&page=1 -->
<c:url value="/board/view" var="detailUrl">
<c:param name="boardId" value="${article.id}" />
<c:param name="page" value="${param.page}" />
<c:param name="keyword" value="${param.keyword}" />
</c:url>
<a href="${detailUrl}">์์ธ ๋ณด๊ธฐ</a>
<!-- ํด๋ผ์ด์ธํธ ๋ฆฌ๋ค์ด๋ ํธ (Context Path ์๋ ํฌํจ) -->
<c:if test="${empty sessionScope.loginUser}">
<c:redirect url="/auth/login">
<c:param name="redirectUrl" value="${pageContext.request.requestURI}" />
</c:redirect>
</c:if>
<!-- ์ธ๋ถ/๋ด๋ถ JSP ์กฐ๊ฐ ์ปดํฌ๋ํธ ์ํฌํธ -->
<c:import url="/WEB-INF/views/common/header.jsp">
<c:param name="pageTitle" value="๋์๋ณด๋" />
</c:import>
```
## 6. ๋ ์ง & ์๊ฐ ์์ํ (fmt:formatDate)
```xml
<!-- ๋ ์ง ํฌ๋งทํ
(java.util.Date ํ์
๊ฐ์ฒด ์ ๋ฌ ํ์) -->
<fmt:formatDate value="${now}" pattern="yyyy-MM-dd HH:mm:ss" />
<!-- ์ถ๋ ฅ: 2026-09-26 14:30:00 -->
<fmt:formatDate value="${article.createdAt}" pattern="yyyy๋
MM์ dd์ผ (E)" />
<!-- ์ถ๋ ฅ: 2026๋
09์ 26์ผ (ํ ) -->
<!-- ์๊ฐ๋ง ์ถ๋ ฅ -->
<fmt:formatDate value="${now}" type="time" timeStyle="short" />
<!-- ํ์์กด ์ง์ (๊ธ๋ก๋ฒ ์๋น์ค KST ๊ณ ์ ) -->
<fmt:setTimeZone value="Asia/Seoul" />
<fmt:formatDate value="${utcDate}" pattern="yyyy-MM-dd HH:mm" />
<!-- ๋ฌธ์์ด ๋ ์ง๋ฅผ Date ๊ฐ์ฒด๋ก ํ์ฑ (fmt:parseDate) -->
<fmt:parseDate value="20260926" pattern="yyyyMMdd" var="parsedDate" />
<fmt:formatDate value="${parsedDate}" pattern="yyyy.MM.dd" />
```
## 7. ์ซ์, ํตํ & ํผ์ผํธ ์์ (fmt:formatNumber)
```xml
<!-- 1. ์ฒ ๋จ์ ์ฝค๋ง ์์ (pattern ์ง์ ) -->
<fmt:formatNumber value="${product.price}" pattern="#,###" /> ์
<!-- ์ถ๋ ฅ: 1,590,000 ์ -->
<!-- 2. ํตํ(Currency) ์์ (๋ก์ผ์ผ ํตํ ๊ธฐํธ ์๋) -->
<fmt:formatNumber value="52000" type="currency" currencySymbol="โฉ" />
<!-- ์ถ๋ ฅ: โฉ52,000 -->
<!-- 3. ๋ฐฑ๋ถ์จ(Percent) ์์ (0.854 -> 85%) -->
<fmt:formatNumber value="${growthRate}" type="percent" />
<!-- 4. ์์์ ์๋ฆฟ์ ๊ณ ์ (์ต์ 2์๋ฆฌ, ์ต๋ 4์๋ฆฌ) -->
<fmt:formatNumber value="12.3" pattern="#,##0.00" />
<!-- ์ถ๋ ฅ: 12.30 -->
<!-- 5. ํฌ๋งท๋ ์ซ์ ๋ฌธ์์ด์ ์์ ์ซ์๋ก ์ญํ์ฑ (fmt:parseNumber) -->
<fmt:parseNumber value="1,250,000" pattern="#,###" var="cleanNumber" />
<!-- ${cleanNumber + 50000} => 1300000 -->
```
## 8. ๋ด์ฅ ๋ฌธ์์ด ํจ์ ๋ชจ์ (fn:*)
```xml
<!-- 1. ์ปฌ๋ ์
/๋ฌธ์์ด ๊ธธ์ด ๊ณ์ฐ (๊ฐ์ฅ ๋น๋ฒํ๊ฒ ์ฌ์ฉ) -->
๋ชฉ๋ก ๊ฐ์: ${fn:length(userList)}๊ฐ
๋น๋ฐ๋ฒํธ ๊ธธ์ด: ${fn:length(user.password)}์
<!-- 2. ๋ฌธ์์ด ํฌํจ ์ฌ๋ถ ๊ฒ์ฌ (๋์๋ฌธ์ ๊ตฌ๋ถ / ๋ฌด์) -->
<c:if test="${fn:contains(user.email, '@company.com')}">์ฌ๋ด ๊ณ์ </c:if>
<c:if test="${fn:containsIgnoreCase(fileName, '.pdf')}">PDF ๋ฌธ์</c:if>
<!-- 3. ์์/๋ ๋ฌธ์์ด ๊ฒ์ฌ -->
<c:if test="${fn:startsWith(url, 'https://')}">๋ณด์ ์ฐ๊ฒฐ</c:if>
<c:if test="${fn:endsWith(fileName, '.png')}">PNG ์ด๋ฏธ์ง</c:if>
<!-- 4. ๋ถ๋ถ ๋ฌธ์์ด ์๋ฅด๊ธฐ & ์์น ์ฐพ๊ธฐ -->
${fn:substring(productCode, 0, 4)} <!-- 0๋ถํฐ 4 ์ด์ ๊น์ง ์ถ์ถ -->
${fn:substringAfter(file, '.')} <!-- ๊ตฌ๋ถ์ ๋ค ๋ฌธ์์ด: ํ์ฅ์ ์ถ์ถ -->
${fn:substringBefore(file, '.')} <!-- ๊ตฌ๋ถ์ ์ ๋ฌธ์์ด: ํ์ผ๋ช
์ถ์ถ -->
${fn:indexOf(description, '์ค์')} <!-- ๋ฌธ์์ด ์์ ์ธ๋ฑ์ค ๋ฐํ (-1 if not found) -->
<!-- 5. ๋ฌธ์์ด ์นํ & ๋ถํ /๊ฒฐํฉ -->
${fn:replace(content, 'Java', 'Kotlin')}
${fn:trim(' ์๋ค ๊ณต๋ฐฑ ์ ๊ฑฐ ')}
<!-- ๋ฐฐ์ด ๋ถํ ํ ๊ฒฐํฉ ์์ -->
<c:set var="tagArr" value="${fn:split('spring,java,jstl', ',')}" />
<c:out value="${fn:join(tagArr, ' #')}" />
```
## 9. EL (Expression Language) ํต์ฌ ์ฐ์ฐ์
```xml
<!-- 1. empty ์ฐ์ฐ์ (null, ๋น ๋ฌธ์์ด, ๋น ์ปฌ๋ ์
ํตํฉ ๊ฒ์ฌ) -->
<!-- null, "", new ArrayList() (size=0), new HashMap() ๋ชจ๋ true -->
<c:if test="${empty boardList}">
<p>๋ฑ๋ก๋ ๊ฒ์๊ธ์ด ์์ต๋๋ค.</p>
</c:if>
<c:if test="${not empty sessionScope.loginUser}">
<p>๋ก๊ทธ์ธ ์ํ์
๋๋ค.</p>
</c:if>
<!-- 2. ๋น๊ต ์ฐ์ฐ์ (๊ธฐํธ ๋๋ ์๋ฌธ ์ถ์ฝ์ด ๋ชจ๋ ์ฌ์ฉ ๊ฐ๋ฅ) -->
${a == b} ๋๋ ${a eq b} <!-- ๋๋ฑ ๋น๊ต (equals ์๋ ํธ์ถ) -->
${a != b} ๋๋ ${a ne b} <!-- ๋ถ์ผ์น -->
${a < b} ๋๋ ${a lt b} <!-- Less Than -->
${a > b} ๋๋ ${a gt b} <!-- Greater Than -->
${a <= b} ๋๋ ${a le b} <!-- Less or Equal -->
${a >= b} ๋๋ ${a ge b} <!-- Greater or Equal -->
<!-- 3. ๋
ผ๋ฆฌ ์ฐ์ฐ์ & ์ผํญ ์ฐ์ฐ์ -->
<c:if test="${user.active and (user.role eq 'ADMIN' or user.role eq 'STAFF')}">
์ ๊ทผ ํ์ฉ
</c:if>
<!-- ์ธ๋ผ์ธ ์กฐ๊ฑด ์ผํญ ์ฐ์ฐ์ -->
<span class="${item.stock > 0 ? 'in-stock' : 'sold-out'}">
${item.stock > 0 ? '๊ตฌ๋งค ๊ฐ๋ฅ' : 'ํ์ '}
</span>
```
## 10. EL ๋ด์ฅ ๊ฐ์ฒด & ์ค์ฝํ ํ์
```xml
<!-- 1. ํ๋ผ๋ฏธํฐ ์ ๊ทผ (HttpServletRequest.getParameter) -->
๊ฒ์์ด: ${param.keyword}
์ ๋ ฌ์์: ${param.sortBy}
์ฒดํฌ๋ฐ์ค ๋ค์ค๊ฐ(๋ฐฐ์ด): ${paramValues.category[0]}
<!-- 2. ์ปจํ
์คํธ ํจ์ค ๋ฐ ์์ฒญ ์ ๋ณด (pageContext) -->
์ปจํ
์คํธ ๋ฃจํธ ๊ฒฝ๋ก: ${pageContext.request.contextPath}
์์ฒญ URI: ${pageContext.request.requestURI}
ํด๋ผ์ด์ธํธ IP: ${pageContext.request.remoteAddr}
<!-- 3. 4๋ ์ค์ฝํ ๋ช
์์ ์ ๊ทผ (์ค์ฝํ ์๋ต ์ page -> request -> session -> application ์ ์๋ ํ์) -->
${pageScope.localVar}
${requestScope.modelAttribute}
${sessionScope.loginUser.id}
${applicationScope.globalConfig}
<!-- 4. ํค๋ & ์ฟ ํค ์ ๊ทผ -->
๋ธ๋ผ์ฐ์ ์ ๋ณด: ${header['User-Agent']}
์ธ์
์ฟ ํค ๊ฐ: ${cookie.JSESSIONID.value}
```
## 11. ์ค๋ฌด ๋น์ถ ํจํด & ํธ๋ฌ๋ธ์ํ
```xml
<!-- 1. ํ
์คํธ ๊ฐํ(์ค๋ฐ๊ฟ \n)์ <br/>๋ก ์นํํ๋ ์ ์ ๊ธฐ๋ฒ -->
<% pageContext.setAttribute("newLineChar", "\n"); %>
${fn:replace(fn:escapeXml(article.content), newLineChar, "<br/>")}
<!-- 2. ์๋ฐ์คํฌ๋ฆฝํธ ์ฝ๋ ๋ด ์์ ํ EL ๋ณ์ ์ฃผ์
(๋ฐ์ดํ ๋๋ฝ ๋ฐฉ์ง & XSS ์๋ฐฉ) -->
<script>
const currentUserId = '<c:out value="${user.id}" />';
const isAuthorized = ${user.isAuthorized ? 'true' : 'false'};
</script>
<!-- 3. ํ์ด์ง ๋ค๋น๊ฒ์ด์
๋ฐ๋ณต ์์ -->
<ul class="pagination">
<c:forEach var="p" begin="${pageInfo.startPage}" end="${pageInfo.endPage}">
<li class="${p eq pageInfo.currentPage ? 'active' : ''}">
<a href="<c:url value="/board/list"><c:param name="page" value="${p}"/></c:url>">${p}</a>
</li>
</c:forEach>
</ul>
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์